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,7 +2,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Label;
2 2
3 public class LabelCreateInputVo 3 public class LabelCreateInputVo
4 { 4 {
5 - public string LabelCode { get; set; } = string.Empty; 5 + public string? LabelCode { get; set; }
6 6
7 public string LabelName { get; set; } = string.Empty; 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,10 +6,33 @@ public class LabelCategoryCreateInputVo
6 6
7 public string CategoryName { get; set; } = string.Empty; 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 public string? CategoryPhotoUrl { get; set; } 17 public string? CategoryPhotoUrl { get; set; }
10 18
11 public bool State { get; set; } = true; 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 public int OrderNum { get; set; } 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,10 +8,16 @@ public class LabelCategoryGetListOutputDto
8 8
9 public string CategoryName { get; set; } = string.Empty; 9 public string CategoryName { get; set; } = string.Empty;
10 10
  11 + public string? DisplayText { get; set; }
  12 +
11 public string? CategoryPhotoUrl { get; set; } 13 public string? CategoryPhotoUrl { get; set; }
12 14
13 public bool State { get; set; } 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 public int OrderNum { get; set; } 21 public int OrderNum { get; set; }
16 22
17 public long NoOfLabels { get; set; } 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,10 +8,18 @@ public class LabelCategoryGetOutputDto
8 8
9 public string CategoryName { get; set; } = string.Empty; 9 public string CategoryName { get; set; } = string.Empty;
10 10
  11 + public string? DisplayText { get; set; }
  12 +
11 public string? CategoryPhotoUrl { get; set; } 13 public string? CategoryPhotoUrl { get; set; }
12 14
13 public bool State { get; set; } 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 public int OrderNum { get; set; } 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,7 +3,7 @@ using System.Text.Json.Serialization;
3 namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate; 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
4 4
5 /// <summary> 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 /// </summary> 7 /// </summary>
8 public class LabelTemplateElementDto 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 namespace FoodLabeling.Application.Contracts.Dtos.Product; 3 namespace FoodLabeling.Application.Contracts.Dtos.Product;
2 4
3 public class ProductCreateInputVo 5 public class ProductCreateInputVo
@@ -11,5 +13,11 @@ public class ProductCreateInputVo @@ -11,5 +13,11 @@ public class ProductCreateInputVo
11 public string? ProductImageUrl { get; set; } 13 public string? ProductImageUrl { get; set; }
12 14
13 public bool State { get; set; } = true; 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,10 +9,33 @@ public class ProductCategoryCreateInputVo
9 9
10 public string CategoryName { get; set; } = string.Empty; 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 public string? CategoryPhotoUrl { get; set; } 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 public bool State { get; set; } = true; 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 public int OrderNum { get; set; } = 0; 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,10 +11,16 @@ public class ProductCategoryGetListOutputDto
11 11
12 public string CategoryName { get; set; } = string.Empty; 12 public string CategoryName { get; set; } = string.Empty;
13 13
  14 + public string? DisplayText { get; set; }
  15 +
14 public string? CategoryPhotoUrl { get; set; } 16 public string? CategoryPhotoUrl { get; set; }
15 17
  18 + public string ButtonAppearance { get; set; } = "TEXT";
  19 +
16 public bool State { get; set; } 20 public bool State { get; set; }
17 21
  22 + public string AvailabilityType { get; set; } = "ALL";
  23 +
18 public int OrderNum { get; set; } 24 public int OrderNum { get; set; }
19 25
20 public DateTime LastEdited { get; set; } 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,10 +11,19 @@ public class ProductCategoryGetOutputDto
11 11
12 public string CategoryName { get; set; } = string.Empty; 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 public string? CategoryPhotoUrl { get; set; } 17 public string? CategoryPhotoUrl { get; set; }
15 18
  19 + public string ButtonAppearance { get; set; } = "TEXT";
  20 +
16 public bool State { get; set; } 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 public int OrderNum { get; set; } 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,6 +11,10 @@ public class RbacMenuGetListOutputDto
11 11
12 public string MenuName { get; set; } = string.Empty; 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 public string? PermissionCode { get; set; } 18 public string? PermissionCode { get; set; }
15 19
16 public int MenuType { get; set; } 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,6 +11,9 @@ public class UsAppLabelCategoryTreeNodeDto
11 11
12 public string? CategoryPhotoUrl { get; set; } 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 public int OrderNum { get; set; } 17 public int OrderNum { get; set; }
15 18
16 public List<UsAppProductCategoryNodeDto> ProductCategories { get; set; } = new(); 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,11 +49,6 @@ public class UsAppLabelPrintInputVo
49 public JsonElement? PrintInputJson { get; set; } 49 public JsonElement? PrintInputJson { get; set; }
50 50
51 /// <summary> 51 /// <summary>
52 - /// 客户端幂等请求 Id(可选);重复相同值时由服务端决定是否直接返回首次结果(见接口文档)。  
53 - /// </summary>  
54 - public string? ClientRequestId { get; set; }  
55 -  
56 - /// <summary>  
57 /// 打印机Id(可选,若业务需要追踪) 52 /// 打印机Id(可选,若业务需要追踪)
58 /// </summary> 53 /// </summary>
59 public string? PrinterId { get; set; } 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,6 +14,18 @@ public class UsAppProductCategoryNodeDto
14 /// <summary>分类显示名;空为「无」</summary> 14 /// <summary>分类显示名;空为「无」</summary>
15 public string Name { get; set; } = string.Empty; 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 public int ItemCount { get; set; } 29 public int ItemCount { get; set; }
18 30
19 public List<UsAppLabelingProductNodeDto> Products { get; set; } = new(); 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,11 +23,18 @@ public interface IProductAppService : IApplicationService
23 /// <summary> 23 /// <summary>
24 /// 新增产品 24 /// 新增产品
25 /// </summary> 25 /// </summary>
  26 + /// <remarks>
  27 + /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。
  28 + /// </remarks>
26 Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); 29 Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input);
27 30
28 /// <summary> 31 /// <summary>
29 /// 编辑产品 32 /// 编辑产品
30 /// </summary> 33 /// </summary>
  34 + /// <remarks>
  35 + /// 当请求体包含 <see cref="ProductCreateInputVo.LocationIds"/> 属性时,按该列表整表替换本产品在各门店的关联;
  36 + /// 不传该属性则不改门店关联(兼容仅改名称/分类等调用)。
  37 + /// </remarks>
31 Task<ProductGetOutputDto> UpdateAsync(string id, ProductUpdateInputVo input); 38 Task<ProductGetOutputDto> UpdateAsync(string id, ProductUpdateInputVo input);
32 39
33 /// <summary> 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,4 +17,35 @@ public interface IUsAppAuthAppService : IApplicationService
17 /// 获取当前登录账号已绑定的门店(用于切换门店等场景) 17 /// 获取当前登录账号已绑定的门店(用于切换门店等场景)
18 /// </summary> 18 /// </summary>
19 Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync(); 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 using FoodLabeling.Application.Contracts.Dtos.Common; 1 using FoodLabeling.Application.Contracts.Dtos.Common;
2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
3 -using FoodLabeling.Application.Contracts.Dtos.Common;  
4 using Volo.Abp.Application.Services; 3 using Volo.Abp.Application.Services;
5 4
6 namespace FoodLabeling.Application.Contracts.IServices; 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,6 +3,7 @@
3 3
4 <ItemGroup> 4 <ItemGroup>
5 <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" /> 5 <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" />
  6 + <PackageReference Include="QuestPDF" Version="2024.12.2" />
6 </ItemGroup> 7 </ItemGroup>
7 8
8 <ItemGroup> 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,10 +24,28 @@ public class FlLabelCategoryDbEntity
24 24
25 public string CategoryName { get; set; } = string.Empty; 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 public string? CategoryPhotoUrl { get; set; } 35 public string? CategoryPhotoUrl { get; set; }
28 36
29 public int OrderNum { get; set; } 37 public int OrderNum { get; set; }
30 38
31 public bool State { get; set; } 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,7 +20,7 @@ public class FlLabelDbEntity
20 20
21 public string ConcurrencyStamp { get; set; } = string.Empty; 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 public string LabelName { get; set; } = string.Empty; 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,10 +24,28 @@ public class FlProductCategoryDbEntity
24 24
25 public string CategoryName { get; set; } = string.Empty; 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 public string? CategoryPhotoUrl { get; set; } 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 public bool State { get; set; } 42 public bool State { get; set; }
30 43
  44 + /// <summary>
  45 + /// 门店可用范围:ALL / SPECIFIED
  46 + /// </summary>
  47 + public string AvailabilityType { get; set; } = "ALL";
  48 +
31 public int OrderNum { get; set; } 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,7 +261,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
261 261
262 return new LabelGetOutputDto 262 return new LabelGetOutputDto
263 { 263 {
264 - Id = label.LabelCode, 264 + Id = label.LabelCode ?? string.Empty,
265 LabelName = label.LabelName, 265 LabelName = label.LabelName,
266 LocationId = label.LocationId ?? string.Empty, 266 LocationId = label.LocationId ?? string.Empty,
267 LocationName = location?.LocationName ?? location?.LocationCode ?? "无", 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,12 +30,34 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
30 var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() 30 var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
31 .Where(x => !x.IsDeleted) 31 .Where(x => !x.IsDeleted)
32 .WhereIF(!string.IsNullOrWhiteSpace(keyword), 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 .WhereIF(input.State != null, x => x.State == input.State); 35 .WhereIF(input.State != null, x => x.State == input.State);
35 36
  37 + // Sorting 仅允许白名单字段,避免 Unknown column/注入风险
36 if (!string.IsNullOrWhiteSpace(input.Sorting)) 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 else 62 else
41 { 63 {
@@ -58,8 +80,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -58,8 +80,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
58 Id = x.Id, 80 Id = x.Id,
59 CategoryCode = x.CategoryCode, 81 CategoryCode = x.CategoryCode,
60 CategoryName = x.CategoryName, 82 CategoryName = x.CategoryName,
  83 + DisplayText = x.DisplayText,
61 CategoryPhotoUrl = x.CategoryPhotoUrl, 84 CategoryPhotoUrl = x.CategoryPhotoUrl,
62 State = x.State, 85 State = x.State,
  86 + ButtonAppearance = x.ButtonAppearance,
  87 + AvailabilityType = x.AvailabilityType,
63 OrderNum = x.OrderNum, 88 OrderNum = x.OrderNum,
64 NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, 89 NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0,
65 LastEdited = x.LastModificationTime ?? x.CreationTime 90 LastEdited = x.LastModificationTime ?? x.CreationTime
@@ -77,7 +102,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -77,7 +102,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
77 throw new UserFriendlyException("标签分类不存在"); 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 public async Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input) 118 public async Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input)
@@ -89,6 +124,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -89,6 +124,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
89 throw new UserFriendlyException("分类编码和名称不能为空"); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() 133 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
93 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); 134 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
94 if (duplicated) 135 if (duplicated)
@@ -96,17 +137,23 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -96,17 +137,23 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
96 throw new UserFriendlyException("分类编码或名称已存在"); 137 throw new UserFriendlyException("分类编码或名称已存在");
97 } 138 }
98 139
  140 + var now = DateTime.Now;
  141 + var currentUserId = CurrentUser?.Id?.ToString();
99 var entity = new FlLabelCategoryDbEntity 142 var entity = new FlLabelCategoryDbEntity
100 { 143 {
101 Id = _guidGenerator.Create().ToString(), 144 Id = _guidGenerator.Create().ToString(),
102 CategoryCode = code, 145 CategoryCode = code,
103 CategoryName = name, 146 CategoryName = name,
104 - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), 147 + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
  148 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
105 State = input.State, 149 State = input.State,
  150 + ButtonAppearance = appearance,
  151 + AvailabilityType = availabilityType,
106 OrderNum = input.OrderNum 152 OrderNum = input.OrderNum
107 }; 153 };
108 154
109 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 155 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  156 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now);
110 return await GetAsync(entity.Id); 157 return await GetAsync(entity.Id);
111 } 158 }
112 159
@@ -126,6 +173,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -126,6 +173,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
126 throw new UserFriendlyException("分类编码和名称不能为空"); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() 182 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
130 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); 183 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
131 if (duplicated) 184 if (duplicated)
@@ -135,13 +188,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -135,13 +188,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
135 188
136 entity.CategoryCode = code; 189 entity.CategoryCode = code;
137 entity.CategoryName = name; 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 entity.State = input.State; 193 entity.State = input.State;
  194 + entity.ButtonAppearance = appearance;
  195 + entity.AvailabilityType = availabilityType;
140 entity.OrderNum = input.OrderNum; 196 entity.OrderNum = input.OrderNum;
141 entity.LastModificationTime = DateTime.Now; 197 entity.LastModificationTime = DateTime.Now;
142 entity.LastModifierId = CurrentUser?.Id?.ToString(); 198 entity.LastModifierId = CurrentUser?.Id?.ToString();
143 199
144 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 200 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  201 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now);
145 return await GetAsync(id); 202 return await GetAsync(id);
146 } 203 }
147 204
@@ -174,12 +231,70 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -174,12 +231,70 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
174 Id = x.Id, 231 Id = x.Id,
175 CategoryCode = x.CategoryCode, 232 CategoryCode = x.CategoryCode,
176 CategoryName = x.CategoryName, 233 CategoryName = x.CategoryName,
  234 + DisplayText = x.DisplayText,
177 CategoryPhotoUrl = x.CategoryPhotoUrl, 235 CategoryPhotoUrl = x.CategoryPhotoUrl,
178 State = x.State, 236 State = x.State,
  237 + ButtonAppearance = x.ButtonAppearance,
  238 + AvailabilityType = x.AvailabilityType,
179 OrderNum = x.OrderNum 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 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) 298 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
184 { 299 {
185 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; 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,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common;
3 using FoodLabeling.Application.Contracts.Dtos.Product; 3 using FoodLabeling.Application.Contracts.Dtos.Product;
4 using FoodLabeling.Application.Contracts.IServices; 4 using FoodLabeling.Application.Contracts.IServices;
5 using FoodLabeling.Application.Services.DbModels; 5 using FoodLabeling.Application.Services.DbModels;
  6 +using FoodLabeling.Domain.Entities;
6 using SqlSugar; 7 using SqlSugar;
7 using Volo.Abp; 8 using Volo.Abp;
8 using Volo.Abp.Application.Services; 9 using Volo.Abp.Application.Services;
@@ -188,6 +189,13 @@ public class ProductAppService : ApplicationService, IProductAppService @@ -188,6 +189,13 @@ public class ProductAppService : ApplicationService, IProductAppService
188 }; 189 };
189 190
190 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 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 return await GetAsync(entity.Id); 199 return await GetAsync(entity.Id);
192 } 200 }
193 201
@@ -228,6 +236,13 @@ public class ProductAppService : ApplicationService, IProductAppService @@ -228,6 +236,13 @@ public class ProductAppService : ApplicationService, IProductAppService
228 entity.State = input.State; 236 entity.State = input.State;
229 237
230 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 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 return await GetAsync(productId); 246 return await GetAsync(productId);
232 } 247 }
233 248
@@ -251,6 +266,66 @@ public class ProductAppService : ApplicationService, IProductAppService @@ -251,6 +266,66 @@ public class ProductAppService : ApplicationService, IProductAppService
251 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 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 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) 329 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
255 { 330 {
256 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; 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 using FoodLabeling.Application.Contracts.Dtos.Common; 2 using FoodLabeling.Application.Contracts.Dtos.Common;
2 using FoodLabeling.Application.Contracts.Dtos.ProductCategory; 3 using FoodLabeling.Application.Contracts.Dtos.ProductCategory;
3 using FoodLabeling.Application.Contracts.IServices; 4 using FoodLabeling.Application.Contracts.IServices;
@@ -35,7 +36,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -35,7 +36,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
35 var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() 36 var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
36 .Where(x => !x.IsDeleted) 37 .Where(x => !x.IsDeleted)
37 .WhereIF(!string.IsNullOrWhiteSpace(keyword), 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 .WhereIF(input.State != null, x => x.State == input.State); 41 .WhereIF(input.State != null, x => x.State == input.State);
40 42
41 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column 43 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column
@@ -77,8 +79,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -77,8 +79,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
77 Id = x.Id, 79 Id = x.Id,
78 CategoryCode = x.CategoryCode, 80 CategoryCode = x.CategoryCode,
79 CategoryName = x.CategoryName, 81 CategoryName = x.CategoryName,
  82 + DisplayText = x.DisplayText,
80 CategoryPhotoUrl = x.CategoryPhotoUrl, 83 CategoryPhotoUrl = x.CategoryPhotoUrl,
  84 + ButtonAppearance = x.ButtonAppearance,
81 State = x.State, 85 State = x.State,
  86 + AvailabilityType = x.AvailabilityType,
82 OrderNum = x.OrderNum, 87 OrderNum = x.OrderNum,
83 LastEdited = x.LastModificationTime ?? x.CreationTime 88 LastEdited = x.LastModificationTime ?? x.CreationTime
84 }).ToList(); 89 }).ToList();
@@ -98,7 +103,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -98,7 +103,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
98 throw new UserFriendlyException("类别不存在"); 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 /// <summary> 119 /// <summary>
@@ -113,6 +128,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -113,6 +128,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
113 throw new UserFriendlyException("类别编码和名称不能为空"); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() 137 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
117 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); 138 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
118 if (duplicated) 139 if (duplicated)
@@ -133,12 +154,16 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -133,12 +154,16 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
133 ConcurrencyStamp = _guidGenerator.Create().ToString("N"), 154 ConcurrencyStamp = _guidGenerator.Create().ToString("N"),
134 CategoryCode = code, 155 CategoryCode = code,
135 CategoryName = name, 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 State = input.State, 160 State = input.State,
  161 + AvailabilityType = availabilityType,
138 OrderNum = input.OrderNum 162 OrderNum = input.OrderNum
139 }; 163 };
140 164
141 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 165 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  166 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now);
142 return await GetAsync(entity.Id); 167 return await GetAsync(entity.Id);
143 } 168 }
144 169
@@ -161,6 +186,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -161,6 +186,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
161 throw new UserFriendlyException("类别编码和名称不能为空"); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() 195 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
165 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); 196 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
166 if (duplicated) 197 if (duplicated)
@@ -170,13 +201,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -170,13 +201,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
170 201
171 entity.CategoryCode = code; 202 entity.CategoryCode = code;
172 entity.CategoryName = name; 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 entity.State = input.State; 207 entity.State = input.State;
  208 + entity.AvailabilityType = availabilityType;
175 entity.OrderNum = input.OrderNum; 209 entity.OrderNum = input.OrderNum;
176 entity.LastModificationTime = DateTime.Now; 210 entity.LastModificationTime = DateTime.Now;
177 entity.LastModifierId = CurrentUser?.Id?.ToString(); 211 entity.LastModifierId = CurrentUser?.Id?.ToString();
178 212
179 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 213 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  214 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now);
180 return await GetAsync(id); 215 return await GetAsync(id);
181 } 216 }
182 217
@@ -213,12 +248,70 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -213,12 +248,70 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
213 Id = x.Id, 248 Id = x.Id,
214 CategoryCode = x.CategoryCode, 249 CategoryCode = x.CategoryCode,
215 CategoryName = x.CategoryName, 250 CategoryName = x.CategoryName,
  251 + DisplayText = x.DisplayText,
216 CategoryPhotoUrl = x.CategoryPhotoUrl, 252 CategoryPhotoUrl = x.CategoryPhotoUrl,
  253 + ButtonAppearance = x.ButtonAppearance,
217 State = x.State, 254 State = x.State,
  255 + AvailabilityType = x.AvailabilityType,
218 OrderNum = x.OrderNum 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 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) 315 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
223 { 316 {
224 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; 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,6 +38,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService
38 Id = x.Id, 38 Id = x.Id,
39 ParentId = x.ParentId, 39 ParentId = x.ParentId,
40 MenuName = x.MenuName ?? string.Empty, 40 MenuName = x.MenuName ?? string.Empty,
  41 + RouterName = x.RouterName,
  42 + Router = x.Router,
41 PermissionCode = x.PermissionCode, 43 PermissionCode = x.PermissionCode,
42 MenuType = x.MenuType, 44 MenuType = x.MenuType,
43 MenuSource = x.MenuSource, 45 MenuSource = x.MenuSource,
@@ -62,6 +64,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService @@ -62,6 +64,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService
62 Id = entity.Id, 64 Id = entity.Id,
63 ParentId = entity.ParentId, 65 ParentId = entity.ParentId,
64 MenuName = entity.MenuName ?? string.Empty, 66 MenuName = entity.MenuName ?? string.Empty,
  67 + RouterName = entity.RouterName,
  68 + Router = entity.Router,
65 PermissionCode = entity.PermissionCode, 69 PermissionCode = entity.PermissionCode,
66 MenuType = entity.MenuType, 70 MenuType = entity.MenuType,
67 MenuSource = entity.MenuSource, 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 using System; 1 using System;
2 using System.Collections.Generic; 2 using System.Collections.Generic;
  3 +using System.Globalization;
3 using System.IdentityModel.Tokens.Jwt; 4 using System.IdentityModel.Tokens.Jwt;
4 using System.Linq; 5 using System.Linq;
5 using System.Security.Claims; 6 using System.Security.Claims;
6 using System.Text; 7 using System.Text;
7 using System.Threading.Tasks; 8 using System.Threading.Tasks;
  9 +using FoodLabeling.Application.Contracts;
8 using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; 10 using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
9 using FoodLabeling.Application.Contracts.IServices; 11 using FoodLabeling.Application.Contracts.IServices;
10 using FoodLabeling.Application.Services.DbModels; 12 using FoodLabeling.Application.Services.DbModels;
@@ -20,6 +22,7 @@ using Volo.Abp; @@ -20,6 +22,7 @@ using Volo.Abp;
20 using Volo.Abp.Application.Services; 22 using Volo.Abp.Application.Services;
21 using Volo.Abp.EventBus.Local; 23 using Volo.Abp.EventBus.Local;
22 using Volo.Abp.Security.Claims; 24 using Volo.Abp.Security.Claims;
  25 +using Volo.Abp.Uow;
23 using Volo.Abp.Users; 26 using Volo.Abp.Users;
24 using Yi.Framework.Core.Helper; 27 using Yi.Framework.Core.Helper;
25 using Yi.Framework.Rbac.Domain.Entities; 28 using Yi.Framework.Rbac.Domain.Entities;
@@ -135,6 +138,267 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -135,6 +138,267 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
135 return await LoadBoundLocationsAsync(CurrentUser.Id.Value); 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 private void ValidationImageCaptcha(string? uuid, string? code) 402 private void ValidationImageCaptcha(string? uuid, string? code)
139 { 403 {
140 if (!_rbacOptions.EnableCaptcha) 404 if (!_rbacOptions.EnableCaptcha)
@@ -149,16 +413,21 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -149,16 +413,21 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
149 } 413 }
150 414
151 /// <summary> 415 /// <summary>
152 - /// 按邮箱查找未删除且启用的用户(邮箱比较忽略大小写) 416 + /// 按邮箱或用户名(邮箱形字符串写在 UserName 时)查找未删除且启用的用户;比较忽略大小写,Email 命中优先。
153 /// </summary> 417 /// </summary>
154 private async Task<UserAggregateRoot?> FindActiveUserByEmailAsync(string email) 418 private async Task<UserAggregateRoot?> FindActiveUserByEmailAsync(string email)
155 { 419 {
156 var normalized = email.Trim().ToLowerInvariant(); 420 var normalized = email.Trim().ToLowerInvariant();
157 var users = await _userRepository._DbQueryable 421 var users = await _userRepository._DbQueryable
158 .Where(u => !u.IsDeleted && u.State == true) 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 .ToListAsync(); 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 private string CreateAppAccessToken(UserAggregateRoot user) 433 private string CreateAppAccessToken(UserAggregateRoot user)
@@ -169,7 +438,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -169,7 +438,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
169 var claims = new List<Claim> 438 var claims = new List<Claim>
170 { 439 {
171 new(AbpClaimTypes.UserId, user.Id.ToString()), 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 if (!string.IsNullOrWhiteSpace(user.Email)) 445 if (!string.IsNullOrWhiteSpace(user.Email))
@@ -257,4 +527,22 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -257,4 +527,22 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
257 527
258 return segments.Count == 0 ? "无" : string.Join(", ", segments); 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,8 +50,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
50 /// 获取当前门店下四级嵌套数据 50 /// 获取当前门店下四级嵌套数据
51 /// </summary> 51 /// </summary>
52 /// <remarks> 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 /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 55 /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。
  56 + /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录;
  57 + /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。
55 /// </remarks> 58 /// </remarks>
56 [Authorize] 59 [Authorize]
57 public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input) 60 public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input)
@@ -83,10 +86,15 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -83,10 +86,15 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
83 LabelCategoryId = c.Id, 86 LabelCategoryId = c.Id,
84 LabelCategoryName = c.CategoryName, 87 LabelCategoryName = c.CategoryName,
85 LabelCategoryPhotoUrl = c.CategoryPhotoUrl, 88 LabelCategoryPhotoUrl = c.CategoryPhotoUrl,
  89 + LabelCategoryButtonAppearance = c.ButtonAppearance,
86 LabelCategoryOrderNum = c.OrderNum, 90 LabelCategoryOrderNum = c.OrderNum,
87 ProductCategoryId = p.CategoryId, 91 ProductCategoryId = p.CategoryId,
88 ProductCategoryName = pc.CategoryName, 92 ProductCategoryName = pc.CategoryName,
89 ProductCategoryPhotoUrl = pc.CategoryPhotoUrl, 93 ProductCategoryPhotoUrl = pc.CategoryPhotoUrl,
  94 + ProductCategoryDisplayText = pc.DisplayText,
  95 + ProductCategoryButtonAppearance = pc.ButtonAppearance,
  96 + ProductCategoryAvailabilityType = pc.AvailabilityType,
  97 + ProductCategoryOrderNum = pc.OrderNum,
90 ProductId = p.Id, 98 ProductId = p.Id,
91 ProductName = p.ProductName, 99 ProductName = p.ProductName,
92 ProductCode = p.ProductCode, 100 ProductCode = p.ProductCode,
@@ -112,17 +120,22 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -112,17 +120,22 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
112 x.LabelCategoryId, 120 x.LabelCategoryId,
113 x.LabelCategoryName, 121 x.LabelCategoryName,
114 x.LabelCategoryPhotoUrl, 122 x.LabelCategoryPhotoUrl,
  123 + x.LabelCategoryButtonAppearance,
115 x.LabelCategoryOrderNum 124 x.LabelCategoryOrderNum
116 }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName); 125 }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName);
117 126
118 var result = new List<UsAppLabelCategoryTreeNodeDto>(); 127 var result = new List<UsAppLabelCategoryTreeNodeDto>();
119 foreach (var g1 in byL1) 128 foreach (var g1 in byL1)
120 { 129 {
  130 + var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance)
  131 + ? "TEXT"
  132 + : g1.Key.LabelCategoryButtonAppearance.Trim();
121 var l1 = new UsAppLabelCategoryTreeNodeDto 133 var l1 = new UsAppLabelCategoryTreeNodeDto
122 { 134 {
123 Id = g1.Key.LabelCategoryId, 135 Id = g1.Key.LabelCategoryId,
124 CategoryName = g1.Key.LabelCategoryName ?? string.Empty, 136 CategoryName = g1.Key.LabelCategoryName ?? string.Empty,
125 CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl, 137 CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl,
  138 + ButtonAppearance = l1Appearance,
126 OrderNum = g1.Key.LabelCategoryOrderNum, 139 OrderNum = g1.Key.LabelCategoryOrderNum,
127 ProductCategories = new List<UsAppProductCategoryNodeDto>() 140 ProductCategories = new List<UsAppProductCategoryNodeDto>()
128 }; 141 };
@@ -136,7 +149,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -136,7 +149,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
136 { 149 {
137 CategoryId = (string?)null, 150 CategoryId = (string?)null,
138 CategoryName = "无", 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,19 +163,34 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
146 { 163 {
147 CategoryId = (string?)categoryId, 164 CategoryId = (string?)categoryId,
148 CategoryName = categoryName, 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 foreach (var g2 in byL2) 176 foreach (var g2 in byL2)
155 { 177 {
156 var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); 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 var l2 = new UsAppProductCategoryNodeDto 185 var l2 = new UsAppProductCategoryNodeDto
158 { 186 {
159 CategoryId = g2.Key.CategoryId, 187 CategoryId = g2.Key.CategoryId,
160 CategoryPhotoUrl = g2.Key.CategoryPhotoUrl, 188 CategoryPhotoUrl = g2.Key.CategoryPhotoUrl,
161 Name = g2.Key.CategoryName, 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 ItemCount = productsGrouped.Count(), 194 ItemCount = productsGrouped.Count(),
163 Products = new List<UsAppLabelingProductNodeDto>() 195 Products = new List<UsAppLabelingProductNodeDto>()
164 }; 196 };
@@ -424,7 +456,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -424,7 +456,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
424 } 456 }
425 457
426 var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId); 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 // 解析模板 elements(与预览一致的渲染数据) 461 // 解析模板 elements(与预览一致的渲染数据)
430 var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo 462 var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
@@ -581,8 +613,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -581,8 +613,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
581 throw new UserFriendlyException("打印任务不存在"); 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 throw new UserFriendlyException("无权限重打该任务"); 620 throw new UserFriendlyException("无权限重打该任务");
588 } 621 }
@@ -852,14 +885,29 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -852,14 +885,29 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
852 .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) 885 .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId)
853 .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) 886 .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State)
854 .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) 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 .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) 895 .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State)
857 .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) 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 .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) 905 .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId)
859 .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => 906 .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) =>
860 (l.LabelName != null && l.LabelName.Contains(keyword!)) || 907 (l.LabelName != null && l.LabelName.Contains(keyword!)) ||
861 (p.ProductName != null && p.ProductName.Contains(keyword!)) || 908 (p.ProductName != null && p.ProductName.Contains(keyword!)) ||
862 (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) || 909 (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) ||
  910 + (pc.DisplayText != null && pc.DisplayText.Contains(keyword!)) ||
863 (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || 911 (c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
864 (t.TypeName != null && t.TypeName.Contains(keyword!)) || 912 (t.TypeName != null && t.TypeName.Contains(keyword!)) ||
865 (l.LabelCode != null && l.LabelCode.Contains(keyword!))); 913 (l.LabelCode != null && l.LabelCode.Contains(keyword!)));
@@ -875,6 +923,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -875,6 +923,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
875 923
876 public string? LabelCategoryPhotoUrl { get; set; } 924 public string? LabelCategoryPhotoUrl { get; set; }
877 925
  926 + public string? LabelCategoryButtonAppearance { get; set; }
  927 +
878 public int LabelCategoryOrderNum { get; set; } 928 public int LabelCategoryOrderNum { get; set; }
879 929
880 public string? ProductCategoryId { get; set; } 930 public string? ProductCategoryId { get; set; }
@@ -883,6 +933,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -883,6 +933,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
883 933
884 public string? ProductCategoryPhotoUrl { get; set; } 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 public string ProductId { get; set; } = string.Empty; 944 public string ProductId { get; set; } = string.Empty;
887 945
888 public string? ProductName { get; set; } 946 public string? ProductName { get; set; }
@@ -908,6 +966,32 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -908,6 +966,32 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
908 public string TemplateUnit { get; set; } = "inch"; 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 private static string NormalizeCategoryName(string? categoryName) 995 private static string NormalizeCategoryName(string? categoryName)
912 { 996 {
913 var s = categoryName?.Trim(); 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,12 +14,6 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
14 Task<bool> RestPasswordAsync(Guid userId, RestPasswordDto input); 14 Task<bool> RestPasswordAsync(Guid userId, RestPasswordDto input);
15 15
16 /// <summary> 16 /// <summary>
17 - /// 提供其他服务使用,根据用户id,直接返回token  
18 - /// </summary>  
19 - /// <returns></returns>  
20 - Task<LoginOutputDto> PostLoginAsync(Guid userId);  
21 -  
22 - /// <summary>  
23 /// 根据信息查询用户,可能为空,代表该用户不存在或禁用 17 /// 根据信息查询用户,可能为空,代表该用户不存在或禁用
24 /// </summary> 18 /// </summary>
25 /// <param name="userName"></param> 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 +6,7 @@ using Microsoft.AspNetCore.Http;
6 using Microsoft.AspNetCore.Mvc; 6 using Microsoft.AspNetCore.Mvc;
7 using Microsoft.Extensions.Caching.Distributed; 7 using Microsoft.Extensions.Caching.Distributed;
8 using Microsoft.Extensions.Options; 8 using Microsoft.Extensions.Options;
  9 +using SqlSugar;
9 using Volo.Abp.Application.Services; 10 using Volo.Abp.Application.Services;
10 using Volo.Abp.Authorization; 11 using Volo.Abp.Authorization;
11 using Volo.Abp.Caching; 12 using Volo.Abp.Caching;
@@ -83,7 +84,7 @@ namespace Yi.Framework.Rbac.Application.Services @@ -83,7 +84,7 @@ namespace Yi.Framework.Rbac.Application.Services
83 //登录不想要验证码 ,可不校验 84 //登录不想要验证码 ,可不校验
84 if (!_captcha.Validate(uuid, code)) 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,21 +98,52 @@ namespace Yi.Framework.Rbac.Application.Services
97 [AllowAnonymous] 98 [AllowAnonymous]
98 public async Task<LoginOutputDto> PostLoginAsync(LoginInputVo input) 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 ValidationImageCaptcha(input.Uuid,input.Code); 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 return await PostLoginAsync(user.Id); 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 /// <summary> 148 /// <summary>
117 /// 提供其他服务使用,根据用户id,直接返回token 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 using System.Web; 2 using System.Web;
3 using NUglify.Helpers; 3 using NUglify.Helpers;
4 using SqlSugar; 4 using SqlSugar;
@@ -7,6 +7,7 @@ using Volo.Abp.Auditing; @@ -7,6 +7,7 @@ using Volo.Abp.Auditing;
7 using Volo.Abp.Domain.Entities; 7 using Volo.Abp.Domain.Entities;
8 using Yi.Framework.Core.Data; 8 using Yi.Framework.Core.Data;
9 using Yi.Framework.Core.Helper; 9 using Yi.Framework.Core.Helper;
  10 +using Yi.Framework.Rbac.Domain.Shared;
10 using Yi.Framework.Rbac.Domain.Shared.Dtos; 11 using Yi.Framework.Rbac.Domain.Shared.Dtos;
11 using Yi.Framework.Rbac.Domain.Shared.Enums; 12 using Yi.Framework.Rbac.Domain.Shared.Enums;
12 13
@@ -25,13 +26,13 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -25,13 +26,13 @@ namespace Yi.Framework.Rbac.Domain.Entities
25 public MenuAggregateRoot(Guid id) 26 public MenuAggregateRoot(Guid id)
26 { 27 {
27 Id = id; 28 Id = id;
28 - ParentId = Guid.Empty; 29 + ParentId = MenuParentIdConverter.FromGuid(Guid.Empty);
29 } 30 }
30 31
31 public MenuAggregateRoot(Guid id, Guid parentId) 32 public MenuAggregateRoot(Guid id, Guid parentId)
32 { 33 {
33 Id = id; 34 Id = id;
34 - ParentId = parentId; 35 + ParentId = MenuParentIdConverter.FromGuid(parentId);
35 } 36 }
36 37
37 /// <summary> 38 /// <summary>
@@ -100,8 +101,9 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -100,8 +101,9 @@ namespace Yi.Framework.Rbac.Domain.Entities
100 /// <summary> 101 /// <summary>
101 /// 102 ///
102 ///</summary> 103 ///</summary>
  104 + /// <summary>父级菜单 Id;库中多为 varchar(如根为 <c>0</c> 或 GUID 文本)。</summary>
103 [SugarColumn(ColumnName = "ParentId")] 105 [SugarColumn(ColumnName = "ParentId")]
104 - public Guid ParentId { get; set; } 106 + public string ParentId { get; set; } = "0";
105 107
106 /// <summary> 108 /// <summary>
107 /// 菜单图标 109 /// 菜单图标
@@ -183,7 +185,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -183,7 +185,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
183 r.OrderNum = m.OrderNum; 185 r.OrderNum = m.OrderNum;
184 var routerName = m.Router?.Split("/").LastOrDefault(); 186 var routerName = m.Router?.Split("/").LastOrDefault();
185 r.Id = m.Id; 187 r.Id = m.Id;
186 - r.ParentId = m.ParentId; 188 + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId);
187 189
188 //开头大写 190 //开头大写
189 r.Name = routerName?.First().ToString().ToUpper() + routerName?.Substring(1); 191 r.Name = routerName?.First().ToString().ToUpper() + routerName?.Substring(1);
@@ -197,7 +199,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -197,7 +199,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
197 r.AlwaysShow = true; 199 r.AlwaysShow = true;
198 200
199 //判断是否为最顶层的路由 201 //判断是否为最顶层的路由
200 - if (Guid.Empty == m.ParentId) 202 + if (MenuParentIdConverter.IsRoot(m.ParentId))
201 { 203 {
202 r.Component = "Layout"; 204 r.Component = "Layout";
203 } 205 }
@@ -250,7 +252,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -250,7 +252,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
250 var r = new Vue3RouterDto(); 252 var r = new Vue3RouterDto();
251 r.OrderNum = m.OrderNum; 253 r.OrderNum = m.OrderNum;
252 r.Id = m.Id; 254 r.Id = m.Id;
253 - r.ParentId = m.ParentId; 255 + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId);
254 r.Hidden = !m.IsShow; 256 r.Hidden = !m.IsShow;
255 257
256 // 检测是否为 URL 链接(http:// 或 https:// 开头) 258 // 检测是否为 URL 链接(http:// 或 https:// 开头)
@@ -359,7 +361,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -359,7 +361,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
359 r.AlwaysShow = false; 361 r.AlwaysShow = false;
360 362
361 // 判断是否为最顶层的路由 363 // 判断是否为最顶层的路由
362 - if (Guid.Empty == m.ParentId) 364 + if (MenuParentIdConverter.IsRoot(m.ParentId))
363 { 365 {
364 r.Component = "Layout"; 366 r.Component = "Layout";
365 } 367 }
@@ -385,7 +387,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -385,7 +387,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
385 r.AlwaysShow = true; 387 r.AlwaysShow = true;
386 388
387 // 判断是否为最顶层的路由 389 // 判断是否为最顶层的路由
388 - if (Guid.Empty == m.ParentId) 390 + if (MenuParentIdConverter.IsRoot(m.ParentId))
389 { 391 {
390 r.Component = "Layout"; 392 r.Component = "Layout";
391 } 393 }
@@ -449,7 +451,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -449,7 +451,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
449 }, 451 },
450 Children =null, 452 Children =null,
451 Id = m.Id, 453 Id = m.Id,
452 - ParentId = m.ParentId 454 + ParentId = MenuParentIdConverter.ToGuid(m.ParentId)
453 }) 455 })
454 .ToList(); 456 .ToList();
455 457
@@ -487,7 +489,7 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -487,7 +489,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
487 var treeDto = new MenuTreeDto 489 var treeDto = new MenuTreeDto
488 { 490 {
489 Id = m.Id, 491 Id = m.Id,
490 - ParentId = m.ParentId, 492 + ParentId = MenuParentIdConverter.ToGuid(m.ParentId),
491 OrderNum = m.OrderNum, 493 OrderNum = m.OrderNum,
492 MenuName = m.MenuName, 494 MenuName = m.MenuName,
493 MenuType = m.MenuType, 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 using System.Security.Claims; 2 using System.Security.Claims;
3 using System.Text; 3 using System.Text;
4 using Mapster; 4 using Mapster;
@@ -77,10 +77,8 @@ namespace Yi.Framework.Rbac.Domain.Managers @@ -77,10 +77,8 @@ namespace Yi.Framework.Rbac.Domain.Managers
77 { 77 {
78 throw new UserFriendlyException(UserConst.No_Role); 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 if (getUserInfo is not null) 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 using Mapster; 2 using Mapster;
3 using Microsoft.Extensions.Caching.Distributed; 3 using Microsoft.Extensions.Caching.Distributed;
4 using Microsoft.Extensions.DependencyInjection; 4 using Microsoft.Extensions.DependencyInjection;
@@ -10,6 +10,7 @@ using Volo.Abp.EventBus.Local; @@ -10,6 +10,7 @@ using Volo.Abp.EventBus.Local;
10 using Volo.Abp.Guids; 10 using Volo.Abp.Guids;
11 using Yi.Framework.Rbac.Domain.Entities; 11 using Yi.Framework.Rbac.Domain.Entities;
12 using Yi.Framework.Rbac.Domain.Repositories; 12 using Yi.Framework.Rbac.Domain.Repositories;
  13 +using Yi.Framework.Rbac.Domain.Shared;
13 using Yi.Framework.Rbac.Domain.Shared.Caches; 14 using Yi.Framework.Rbac.Domain.Shared.Caches;
14 using Yi.Framework.Rbac.Domain.Shared.Consts; 15 using Yi.Framework.Rbac.Domain.Shared.Consts;
15 using Yi.Framework.Rbac.Domain.Shared.Dtos; 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,7 +87,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
87 Router = "/system/user/index", 87 Router = "/system/user/index",
88 MenuIcon = "ri:admin-line", 88 MenuIcon = "ri:admin-line",
89 OrderNum = 100, 89 OrderNum = 100,
90 - ParentId = system.Id, 90 + ParentId = system.Id.ToString(),
91 RouterName = "SystemUser" 91 RouterName = "SystemUser"
92 }; 92 };
93 entities.Add(user); 93 entities.Add(user);
@@ -99,7 +99,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -99,7 +99,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
99 PermissionCode = "system:user:query", 99 PermissionCode = "system:user:query",
100 MenuType = MenuTypeEnum.Component, 100 MenuType = MenuTypeEnum.Component,
101 OrderNum = 100, 101 OrderNum = 100,
102 - ParentId = user.Id, 102 + ParentId = user.Id.ToString(),
103 IsDeleted = false 103 IsDeleted = false
104 }; 104 };
105 entities.Add(userQuery); 105 entities.Add(userQuery);
@@ -111,7 +111,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -111,7 +111,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
111 PermissionCode = "system:user:add", 111 PermissionCode = "system:user:add",
112 MenuType = MenuTypeEnum.Component, 112 MenuType = MenuTypeEnum.Component,
113 OrderNum = 100, 113 OrderNum = 100,
114 - ParentId = user.Id, 114 + ParentId = user.Id.ToString(),
115 IsDeleted = false 115 IsDeleted = false
116 }; 116 };
117 entities.Add(userAdd); 117 entities.Add(userAdd);
@@ -123,7 +123,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -123,7 +123,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
123 PermissionCode = "system:user:edit", 123 PermissionCode = "system:user:edit",
124 MenuType = MenuTypeEnum.Component, 124 MenuType = MenuTypeEnum.Component,
125 OrderNum = 100, 125 OrderNum = 100,
126 - ParentId = user.Id, 126 + ParentId = user.Id.ToString(),
127 IsDeleted = false 127 IsDeleted = false
128 }; 128 };
129 entities.Add(userEdit); 129 entities.Add(userEdit);
@@ -135,7 +135,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -135,7 +135,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
135 PermissionCode = "system:user:remove", 135 PermissionCode = "system:user:remove",
136 MenuType = MenuTypeEnum.Component, 136 MenuType = MenuTypeEnum.Component,
137 OrderNum = 100, 137 OrderNum = 100,
138 - ParentId = user.Id, 138 + ParentId = user.Id.ToString(),
139 IsDeleted = false 139 IsDeleted = false
140 }; 140 };
141 entities.Add(userRemove); 141 entities.Add(userRemove);
@@ -148,7 +148,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -148,7 +148,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
148 PermissionCode = "system:user:resetPwd", 148 PermissionCode = "system:user:resetPwd",
149 MenuType = MenuTypeEnum.Component, 149 MenuType = MenuTypeEnum.Component,
150 OrderNum = 100, 150 OrderNum = 100,
151 - ParentId = user.Id, 151 + ParentId = user.Id.ToString(),
152 IsDeleted = false 152 IsDeleted = false
153 }; 153 };
154 entities.Add(userResetPwd); 154 entities.Add(userResetPwd);
@@ -164,7 +164,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -164,7 +164,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
164 Router = "/system/role/index", 164 Router = "/system/role/index",
165 MenuIcon = "ri:admin-fill", 165 MenuIcon = "ri:admin-fill",
166 OrderNum = 99, 166 OrderNum = 99,
167 - ParentId = system.Id, 167 + ParentId = system.Id.ToString(),
168 RouterName = "SystemRole" 168 RouterName = "SystemRole"
169 }; 169 };
170 entities.Add(role); 170 entities.Add(role);
@@ -176,7 +176,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -176,7 +176,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
176 PermissionCode = "system:role:query", 176 PermissionCode = "system:role:query",
177 MenuType = MenuTypeEnum.Component, 177 MenuType = MenuTypeEnum.Component,
178 OrderNum = 100, 178 OrderNum = 100,
179 - ParentId = role.Id, 179 + ParentId = role.Id.ToString(),
180 IsDeleted = false 180 IsDeleted = false
181 }; 181 };
182 entities.Add(roleQuery); 182 entities.Add(roleQuery);
@@ -188,7 +188,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -188,7 +188,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
188 PermissionCode = "system:role:add", 188 PermissionCode = "system:role:add",
189 MenuType = MenuTypeEnum.Component, 189 MenuType = MenuTypeEnum.Component,
190 OrderNum = 100, 190 OrderNum = 100,
191 - ParentId = role.Id, 191 + ParentId = role.Id.ToString(),
192 IsDeleted = false 192 IsDeleted = false
193 }; 193 };
194 entities.Add(roleAdd); 194 entities.Add(roleAdd);
@@ -200,7 +200,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -200,7 +200,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
200 PermissionCode = "system:role:edit", 200 PermissionCode = "system:role:edit",
201 MenuType = MenuTypeEnum.Component, 201 MenuType = MenuTypeEnum.Component,
202 OrderNum = 100, 202 OrderNum = 100,
203 - ParentId = role.Id, 203 + ParentId = role.Id.ToString(),
204 IsDeleted = false 204 IsDeleted = false
205 }; 205 };
206 entities.Add(roleEdit); 206 entities.Add(roleEdit);
@@ -212,7 +212,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -212,7 +212,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
212 PermissionCode = "system:role:remove", 212 PermissionCode = "system:role:remove",
213 MenuType = MenuTypeEnum.Component, 213 MenuType = MenuTypeEnum.Component,
214 OrderNum = 100, 214 OrderNum = 100,
215 - ParentId = role.Id, 215 + ParentId = role.Id.ToString(),
216 IsDeleted = false 216 IsDeleted = false
217 }; 217 };
218 entities.Add(roleRemove); 218 entities.Add(roleRemove);
@@ -228,7 +228,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -228,7 +228,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
228 Router = "/system/menu/index", 228 Router = "/system/menu/index",
229 MenuIcon = "ep:menu", 229 MenuIcon = "ep:menu",
230 OrderNum = 98, 230 OrderNum = 98,
231 - ParentId = system.Id, 231 + ParentId = system.Id.ToString(),
232 RouterName = "SystemMenu" 232 RouterName = "SystemMenu"
233 }; 233 };
234 entities.Add(menu); 234 entities.Add(menu);
@@ -240,7 +240,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -240,7 +240,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
240 PermissionCode = "system:menu:query", 240 PermissionCode = "system:menu:query",
241 MenuType = MenuTypeEnum.Component, 241 MenuType = MenuTypeEnum.Component,
242 OrderNum = 100, 242 OrderNum = 100,
243 - ParentId = menu.Id, 243 + ParentId = menu.Id.ToString(),
244 IsDeleted = false 244 IsDeleted = false
245 }; 245 };
246 entities.Add(menuQuery); 246 entities.Add(menuQuery);
@@ -252,7 +252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -252,7 +252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
252 PermissionCode = "system:menu:add", 252 PermissionCode = "system:menu:add",
253 MenuType = MenuTypeEnum.Component, 253 MenuType = MenuTypeEnum.Component,
254 OrderNum = 100, 254 OrderNum = 100,
255 - ParentId = menu.Id, 255 + ParentId = menu.Id.ToString(),
256 IsDeleted = false 256 IsDeleted = false
257 }; 257 };
258 entities.Add(menuAdd); 258 entities.Add(menuAdd);
@@ -264,7 +264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -264,7 +264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
264 PermissionCode = "system:menu:edit", 264 PermissionCode = "system:menu:edit",
265 MenuType = MenuTypeEnum.Component, 265 MenuType = MenuTypeEnum.Component,
266 OrderNum = 100, 266 OrderNum = 100,
267 - ParentId = menu.Id, 267 + ParentId = menu.Id.ToString(),
268 IsDeleted = false 268 IsDeleted = false
269 }; 269 };
270 entities.Add(menuEdit); 270 entities.Add(menuEdit);
@@ -276,7 +276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -276,7 +276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
276 PermissionCode = "system:menu:remove", 276 PermissionCode = "system:menu:remove",
277 MenuType = MenuTypeEnum.Component, 277 MenuType = MenuTypeEnum.Component,
278 OrderNum = 100, 278 OrderNum = 100,
279 - ParentId = menu.Id, 279 + ParentId = menu.Id.ToString(),
280 IsDeleted = false 280 IsDeleted = false
281 }; 281 };
282 entities.Add(menuRemove); 282 entities.Add(menuRemove);
@@ -291,7 +291,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -291,7 +291,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
291 Router = "/system/dept/index", 291 Router = "/system/dept/index",
292 MenuIcon = "ri:git-branch-line", 292 MenuIcon = "ri:git-branch-line",
293 OrderNum = 97, 293 OrderNum = 97,
294 - ParentId = system.Id, 294 + ParentId = system.Id.ToString(),
295 RouterName = "SystemDept" 295 RouterName = "SystemDept"
296 }; 296 };
297 entities.Add(dept); 297 entities.Add(dept);
@@ -303,7 +303,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -303,7 +303,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
303 PermissionCode = "system:dept:query", 303 PermissionCode = "system:dept:query",
304 MenuType = MenuTypeEnum.Component, 304 MenuType = MenuTypeEnum.Component,
305 OrderNum = 100, 305 OrderNum = 100,
306 - ParentId = dept.Id, 306 + ParentId = dept.Id.ToString(),
307 IsDeleted = false 307 IsDeleted = false
308 }; 308 };
309 entities.Add(deptQuery); 309 entities.Add(deptQuery);
@@ -315,7 +315,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -315,7 +315,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
315 PermissionCode = "system:dept:add", 315 PermissionCode = "system:dept:add",
316 MenuType = MenuTypeEnum.Component, 316 MenuType = MenuTypeEnum.Component,
317 OrderNum = 100, 317 OrderNum = 100,
318 - ParentId = dept.Id, 318 + ParentId = dept.Id.ToString(),
319 IsDeleted = false 319 IsDeleted = false
320 }; 320 };
321 entities.Add(deptAdd); 321 entities.Add(deptAdd);
@@ -327,7 +327,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -327,7 +327,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
327 PermissionCode = "system:dept:edit", 327 PermissionCode = "system:dept:edit",
328 MenuType = MenuTypeEnum.Component, 328 MenuType = MenuTypeEnum.Component,
329 OrderNum = 100, 329 OrderNum = 100,
330 - ParentId = dept.Id, 330 + ParentId = dept.Id.ToString(),
331 IsDeleted = false 331 IsDeleted = false
332 }; 332 };
333 entities.Add(deptEdit); 333 entities.Add(deptEdit);
@@ -339,7 +339,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -339,7 +339,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
339 PermissionCode = "system:dept:remove", 339 PermissionCode = "system:dept:remove",
340 MenuType = MenuTypeEnum.Component, 340 MenuType = MenuTypeEnum.Component,
341 OrderNum = 100, 341 OrderNum = 100,
342 - ParentId = dept.Id, 342 + ParentId = dept.Id.ToString(),
343 IsDeleted = false 343 IsDeleted = false
344 }; 344 };
345 entities.Add(deptRemove); 345 entities.Add(deptRemove);
@@ -356,7 +356,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -356,7 +356,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
356 Router = "/system/post/index", 356 Router = "/system/post/index",
357 MenuIcon = "ant-design:deployment-unit-outlined", 357 MenuIcon = "ant-design:deployment-unit-outlined",
358 OrderNum = 96, 358 OrderNum = 96,
359 - ParentId = system.Id, 359 + ParentId = system.Id.ToString(),
360 RouterName = "SystemPost" 360 RouterName = "SystemPost"
361 }; 361 };
362 entities.Add(post); 362 entities.Add(post);
@@ -368,7 +368,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -368,7 +368,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
368 PermissionCode = "system:post:query", 368 PermissionCode = "system:post:query",
369 MenuType = MenuTypeEnum.Component, 369 MenuType = MenuTypeEnum.Component,
370 OrderNum = 100, 370 OrderNum = 100,
371 - ParentId = post.Id, 371 + ParentId = post.Id.ToString(),
372 IsDeleted = false 372 IsDeleted = false
373 }; 373 };
374 entities.Add(postQuery); 374 entities.Add(postQuery);
@@ -380,7 +380,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -380,7 +380,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
380 PermissionCode = "system:post:add", 380 PermissionCode = "system:post:add",
381 MenuType = MenuTypeEnum.Component, 381 MenuType = MenuTypeEnum.Component,
382 OrderNum = 100, 382 OrderNum = 100,
383 - ParentId = post.Id, 383 + ParentId = post.Id.ToString(),
384 IsDeleted = false 384 IsDeleted = false
385 }; 385 };
386 entities.Add(postAdd); 386 entities.Add(postAdd);
@@ -392,7 +392,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -392,7 +392,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
392 PermissionCode = "system:post:edit", 392 PermissionCode = "system:post:edit",
393 MenuType = MenuTypeEnum.Component, 393 MenuType = MenuTypeEnum.Component,
394 OrderNum = 100, 394 OrderNum = 100,
395 - ParentId = post.Id, 395 + ParentId = post.Id.ToString(),
396 IsDeleted = false 396 IsDeleted = false
397 }; 397 };
398 entities.Add(postEdit); 398 entities.Add(postEdit);
@@ -404,7 +404,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -404,7 +404,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
404 PermissionCode = "system:post:remove", 404 PermissionCode = "system:post:remove",
405 MenuType = MenuTypeEnum.Component, 405 MenuType = MenuTypeEnum.Component,
406 OrderNum = 100, 406 OrderNum = 100,
407 - ParentId = post.Id, 407 + ParentId = post.Id.ToString(),
408 IsDeleted = false 408 IsDeleted = false
409 }; 409 };
410 entities.Add(postRemove); 410 entities.Add(postRemove);
@@ -420,7 +420,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -420,7 +420,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
420 Router = "/monitor/operation-logs", 420 Router = "/monitor/operation-logs",
421 MenuIcon = "ri:history-fill", 421 MenuIcon = "ri:history-fill",
422 OrderNum = 100, 422 OrderNum = 100,
423 - ParentId = monitoring.Id, 423 + ParentId = monitoring.Id.ToString(),
424 RouterName = "OperationLog", 424 RouterName = "OperationLog",
425 Component = "monitor/logs/operation/index" 425 Component = "monitor/logs/operation/index"
426 }; 426 };
@@ -433,7 +433,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -433,7 +433,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
433 PermissionCode = "monitor:operlog:query", 433 PermissionCode = "monitor:operlog:query",
434 MenuType = MenuTypeEnum.Component, 434 MenuType = MenuTypeEnum.Component,
435 OrderNum = 100, 435 OrderNum = 100,
436 - ParentId = operationLog.Id, 436 + ParentId = operationLog.Id.ToString(),
437 IsDeleted = false 437 IsDeleted = false
438 }; 438 };
439 entities.Add(operationLogQuery); 439 entities.Add(operationLogQuery);
@@ -445,7 +445,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -445,7 +445,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
445 PermissionCode = "monitor:operlog:remove", 445 PermissionCode = "monitor:operlog:remove",
446 MenuType = MenuTypeEnum.Component, 446 MenuType = MenuTypeEnum.Component,
447 OrderNum = 100, 447 OrderNum = 100,
448 - ParentId = operationLog.Id, 448 + ParentId = operationLog.Id.ToString(),
449 IsDeleted = false 449 IsDeleted = false
450 }; 450 };
451 entities.Add(operationLogRemove); 451 entities.Add(operationLogRemove);
@@ -465,7 +465,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -465,7 +465,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
465 Component = "monitor/logs/login/index", 465 Component = "monitor/logs/login/index",
466 MenuIcon = "ri:window-line", 466 MenuIcon = "ri:window-line",
467 OrderNum = 100, 467 OrderNum = 100,
468 - ParentId = monitoring.Id, 468 + ParentId = monitoring.Id.ToString(),
469 RouterName = "LoginLog", 469 RouterName = "LoginLog",
470 }; 470 };
471 entities.Add(loginLog); 471 entities.Add(loginLog);
@@ -477,7 +477,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -477,7 +477,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
477 PermissionCode = "monitor:logininfor:query", 477 PermissionCode = "monitor:logininfor:query",
478 MenuType = MenuTypeEnum.Component, 478 MenuType = MenuTypeEnum.Component,
479 OrderNum = 100, 479 OrderNum = 100,
480 - ParentId = loginLog.Id, 480 + ParentId = loginLog.Id.ToString(),
481 IsDeleted = false 481 IsDeleted = false
482 }; 482 };
483 entities.Add(loginLogQuery); 483 entities.Add(loginLogQuery);
@@ -489,7 +489,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -489,7 +489,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
489 PermissionCode = "monitor:logininfor:remove", 489 PermissionCode = "monitor:logininfor:remove",
490 MenuType = MenuTypeEnum.Component, 490 MenuType = MenuTypeEnum.Component,
491 OrderNum = 100, 491 OrderNum = 100,
492 - ParentId = loginLog.Id, 492 + ParentId = loginLog.Id.ToString(),
493 IsDeleted = false, 493 IsDeleted = false,
494 494
495 }; 495 };
@@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
509 Component = "/system/config/index", 509 Component = "/system/config/index",
510 MenuIcon = "ri:edit-box-line", 510 MenuIcon = "ri:edit-box-line",
511 OrderNum = 94, 511 OrderNum = 94,
512 - ParentId = system.Id, 512 + ParentId = system.Id.ToString(),
513 IsDeleted = false 513 IsDeleted = false
514 }; 514 };
515 entities.Add(config); 515 entities.Add(config);
@@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
521 PermissionCode = "system:config:query", 521 PermissionCode = "system:config:query",
522 MenuType = MenuTypeEnum.Component, 522 MenuType = MenuTypeEnum.Component,
523 OrderNum = 100, 523 OrderNum = 100,
524 - ParentId = config.Id, 524 + ParentId = config.Id.ToString(),
525 IsDeleted = false 525 IsDeleted = false
526 }; 526 };
527 entities.Add(configQuery); 527 entities.Add(configQuery);
@@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
533 PermissionCode = "system:config:add", 533 PermissionCode = "system:config:add",
534 MenuType = MenuTypeEnum.Component, 534 MenuType = MenuTypeEnum.Component,
535 OrderNum = 100, 535 OrderNum = 100,
536 - ParentId = config.Id, 536 + ParentId = config.Id.ToString(),
537 IsDeleted = false 537 IsDeleted = false
538 }; 538 };
539 entities.Add(configAdd); 539 entities.Add(configAdd);
@@ -545,7 +545,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -545,7 +545,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
545 PermissionCode = "system:config:edit", 545 PermissionCode = "system:config:edit",
546 MenuType = MenuTypeEnum.Component, 546 MenuType = MenuTypeEnum.Component,
547 OrderNum = 100, 547 OrderNum = 100,
548 - ParentId = config.Id, 548 + ParentId = config.Id.ToString(),
549 IsDeleted = false 549 IsDeleted = false
550 }; 550 };
551 entities.Add(configEdit); 551 entities.Add(configEdit);
@@ -557,7 +557,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -557,7 +557,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
557 PermissionCode = "system:config:remove", 557 PermissionCode = "system:config:remove",
558 MenuType = MenuTypeEnum.Component, 558 MenuType = MenuTypeEnum.Component,
559 OrderNum = 100, 559 OrderNum = 100,
560 - ParentId = config.Id, 560 + ParentId = config.Id.ToString(),
561 IsDeleted = false 561 IsDeleted = false
562 }; 562 };
563 entities.Add(configRemove); 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,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
88 Component = "code/field/index", 88 Component = "code/field/index",
89 MenuIcon = "number", 89 MenuIcon = "number",
90 OrderNum = 99, 90 OrderNum = 99,
91 - ParentId = code.Id, 91 + ParentId = code.Id.ToString(),
92 IsDeleted = false 92 IsDeleted = false
93 }; 93 };
94 entities.Add(field); 94 entities.Add(field);
@@ -599,7 +599,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -599,7 +599,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
599 Component = "system/tenant/index", 599 Component = "system/tenant/index",
600 MenuIcon = "list", 600 MenuIcon = "list",
601 OrderNum = 101, 601 OrderNum = 101,
602 - ParentId = system.Id, 602 + ParentId = system.Id.ToString(),
603 IsDeleted = false 603 IsDeleted = false
604 }; 604 };
605 entities.Add(tenant); 605 entities.Add(tenant);
@@ -611,7 +611,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -611,7 +611,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
611 PermissionCode = "system:tenant:query", 611 PermissionCode = "system:tenant:query",
612 MenuType = MenuTypeEnum.Component, 612 MenuType = MenuTypeEnum.Component,
613 OrderNum = 100, 613 OrderNum = 100,
614 - ParentId = tenant.Id, 614 + ParentId = tenant.Id.ToString(),
615 IsDeleted = false 615 IsDeleted = false
616 }; 616 };
617 entities.Add(tenantQuery); 617 entities.Add(tenantQuery);
@@ -623,7 +623,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -623,7 +623,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
623 PermissionCode = "system:tenant:add", 623 PermissionCode = "system:tenant:add",
624 MenuType = MenuTypeEnum.Component, 624 MenuType = MenuTypeEnum.Component,
625 OrderNum = 100, 625 OrderNum = 100,
626 - ParentId = tenant.Id, 626 + ParentId = tenant.Id.ToString(),
627 IsDeleted = false 627 IsDeleted = false
628 }; 628 };
629 entities.Add(tenantAdd); 629 entities.Add(tenantAdd);
@@ -635,7 +635,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -635,7 +635,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
635 PermissionCode = "system:tenant:edit", 635 PermissionCode = "system:tenant:edit",
636 MenuType = MenuTypeEnum.Component, 636 MenuType = MenuTypeEnum.Component,
637 OrderNum = 100, 637 OrderNum = 100,
638 - ParentId = tenant.Id, 638 + ParentId = tenant.Id.ToString(),
639 IsDeleted = false 639 IsDeleted = false
640 }; 640 };
641 entities.Add(tenantEdit); 641 entities.Add(tenantEdit);
@@ -647,7 +647,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -647,7 +647,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
647 PermissionCode = "system:tenant:remove", 647 PermissionCode = "system:tenant:remove",
648 MenuType = MenuTypeEnum.Component, 648 MenuType = MenuTypeEnum.Component,
649 OrderNum = 100, 649 OrderNum = 100,
650 - ParentId = tenant.Id, 650 + ParentId = tenant.Id.ToString(),
651 IsDeleted = false 651 IsDeleted = false
652 }; 652 };
653 entities.Add(tenantRemove); 653 entities.Add(tenantRemove);
@@ -677,7 +677,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -677,7 +677,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
677 Component = "system/user/index", 677 Component = "system/user/index",
678 MenuIcon = "user", 678 MenuIcon = "user",
679 OrderNum = 100, 679 OrderNum = 100,
680 - ParentId = system.Id, 680 + ParentId = system.Id.ToString(),
681 IsDeleted = false 681 IsDeleted = false
682 }; 682 };
683 entities.Add(user); 683 entities.Add(user);
@@ -689,7 +689,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -689,7 +689,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
689 PermissionCode = "system:user:query", 689 PermissionCode = "system:user:query",
690 MenuType = MenuTypeEnum.Component, 690 MenuType = MenuTypeEnum.Component,
691 OrderNum = 100, 691 OrderNum = 100,
692 - ParentId = user.Id, 692 + ParentId = user.Id.ToString(),
693 IsDeleted = false 693 IsDeleted = false
694 }; 694 };
695 entities.Add(userQuery); 695 entities.Add(userQuery);
@@ -701,7 +701,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -701,7 +701,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
701 PermissionCode = "system:user:add", 701 PermissionCode = "system:user:add",
702 MenuType = MenuTypeEnum.Component, 702 MenuType = MenuTypeEnum.Component,
703 OrderNum = 100, 703 OrderNum = 100,
704 - ParentId = user.Id, 704 + ParentId = user.Id.ToString(),
705 IsDeleted = false 705 IsDeleted = false
706 }; 706 };
707 entities.Add(userAdd); 707 entities.Add(userAdd);
@@ -713,7 +713,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -713,7 +713,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
713 PermissionCode = "system:user:edit", 713 PermissionCode = "system:user:edit",
714 MenuType = MenuTypeEnum.Component, 714 MenuType = MenuTypeEnum.Component,
715 OrderNum = 100, 715 OrderNum = 100,
716 - ParentId = user.Id, 716 + ParentId = user.Id.ToString(),
717 IsDeleted = false 717 IsDeleted = false
718 }; 718 };
719 entities.Add(userEdit); 719 entities.Add(userEdit);
@@ -725,7 +725,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -725,7 +725,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
725 PermissionCode = "system:user:remove", 725 PermissionCode = "system:user:remove",
726 MenuType = MenuTypeEnum.Component, 726 MenuType = MenuTypeEnum.Component,
727 OrderNum = 100, 727 OrderNum = 100,
728 - ParentId = user.Id, 728 + ParentId = user.Id.ToString(),
729 IsDeleted = false 729 IsDeleted = false
730 }; 730 };
731 entities.Add(userRemove); 731 entities.Add(userRemove);
@@ -738,7 +738,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -738,7 +738,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
738 PermissionCode = "system:user:resetPwd", 738 PermissionCode = "system:user:resetPwd",
739 MenuType = MenuTypeEnum.Component, 739 MenuType = MenuTypeEnum.Component,
740 OrderNum = 100, 740 OrderNum = 100,
741 - ParentId = user.Id, 741 + ParentId = user.Id.ToString(),
742 IsDeleted = false 742 IsDeleted = false
743 }; 743 };
744 entities.Add(userResetPwd); 744 entities.Add(userResetPwd);
@@ -758,7 +758,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -758,7 +758,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
758 Component = "system/role/index", 758 Component = "system/role/index",
759 MenuIcon = "peoples", 759 MenuIcon = "peoples",
760 OrderNum = 99, 760 OrderNum = 99,
761 - ParentId = system.Id, 761 + ParentId = system.Id.ToString(),
762 IsDeleted = false 762 IsDeleted = false
763 }; 763 };
764 entities.Add(role); 764 entities.Add(role);
@@ -770,7 +770,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -770,7 +770,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
770 PermissionCode = "system:role:query", 770 PermissionCode = "system:role:query",
771 MenuType = MenuTypeEnum.Component, 771 MenuType = MenuTypeEnum.Component,
772 OrderNum = 100, 772 OrderNum = 100,
773 - ParentId = role.Id, 773 + ParentId = role.Id.ToString(),
774 IsDeleted = false 774 IsDeleted = false
775 }; 775 };
776 entities.Add(roleQuery); 776 entities.Add(roleQuery);
@@ -782,7 +782,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -782,7 +782,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
782 PermissionCode = "system:role:add", 782 PermissionCode = "system:role:add",
783 MenuType = MenuTypeEnum.Component, 783 MenuType = MenuTypeEnum.Component,
784 OrderNum = 100, 784 OrderNum = 100,
785 - ParentId = role.Id, 785 + ParentId = role.Id.ToString(),
786 IsDeleted = false 786 IsDeleted = false
787 }; 787 };
788 entities.Add(roleAdd); 788 entities.Add(roleAdd);
@@ -794,7 +794,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -794,7 +794,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
794 PermissionCode = "system:role:edit", 794 PermissionCode = "system:role:edit",
795 MenuType = MenuTypeEnum.Component, 795 MenuType = MenuTypeEnum.Component,
796 OrderNum = 100, 796 OrderNum = 100,
797 - ParentId = role.Id, 797 + ParentId = role.Id.ToString(),
798 IsDeleted = false 798 IsDeleted = false
799 }; 799 };
800 entities.Add(roleEdit); 800 entities.Add(roleEdit);
@@ -806,7 +806,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -806,7 +806,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
806 PermissionCode = "system:role:remove", 806 PermissionCode = "system:role:remove",
807 MenuType = MenuTypeEnum.Component, 807 MenuType = MenuTypeEnum.Component,
808 OrderNum = 100, 808 OrderNum = 100,
809 - ParentId = role.Id, 809 + ParentId = role.Id.ToString(),
810 IsDeleted = false 810 IsDeleted = false
811 }; 811 };
812 entities.Add(roleRemove); 812 entities.Add(roleRemove);
@@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
826 Component = "system/menu/index", 826 Component = "system/menu/index",
827 MenuIcon = "tree-table", 827 MenuIcon = "tree-table",
828 OrderNum = 98, 828 OrderNum = 98,
829 - ParentId = system.Id, 829 + ParentId = system.Id.ToString(),
830 IsDeleted = false 830 IsDeleted = false
831 }; 831 };
832 entities.Add(menu); 832 entities.Add(menu);
@@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
838 PermissionCode = "system:menu:query", 838 PermissionCode = "system:menu:query",
839 MenuType = MenuTypeEnum.Component, 839 MenuType = MenuTypeEnum.Component,
840 OrderNum = 100, 840 OrderNum = 100,
841 - ParentId = menu.Id, 841 + ParentId = menu.Id.ToString(),
842 IsDeleted = false 842 IsDeleted = false
843 }; 843 };
844 entities.Add(menuQuery); 844 entities.Add(menuQuery);
@@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
850 PermissionCode = "system:menu:add", 850 PermissionCode = "system:menu:add",
851 MenuType = MenuTypeEnum.Component, 851 MenuType = MenuTypeEnum.Component,
852 OrderNum = 100, 852 OrderNum = 100,
853 - ParentId = menu.Id, 853 + ParentId = menu.Id.ToString(),
854 IsDeleted = false 854 IsDeleted = false
855 }; 855 };
856 entities.Add(menuAdd); 856 entities.Add(menuAdd);
@@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
862 PermissionCode = "system:menu:edit", 862 PermissionCode = "system:menu:edit",
863 MenuType = MenuTypeEnum.Component, 863 MenuType = MenuTypeEnum.Component,
864 OrderNum = 100, 864 OrderNum = 100,
865 - ParentId = menu.Id, 865 + ParentId = menu.Id.ToString(),
866 IsDeleted = false 866 IsDeleted = false
867 }; 867 };
868 entities.Add(menuEdit); 868 entities.Add(menuEdit);
@@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
874 PermissionCode = "system:menu:remove", 874 PermissionCode = "system:menu:remove",
875 MenuType = MenuTypeEnum.Component, 875 MenuType = MenuTypeEnum.Component,
876 OrderNum = 100, 876 OrderNum = 100,
877 - ParentId = menu.Id, 877 + ParentId = menu.Id.ToString(),
878 IsDeleted = false 878 IsDeleted = false
879 }; 879 };
880 entities.Add(menuRemove); 880 entities.Add(menuRemove);
@@ -893,7 +893,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -893,7 +893,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
893 Component = "system/dept/index", 893 Component = "system/dept/index",
894 MenuIcon = "tree", 894 MenuIcon = "tree",
895 OrderNum = 97, 895 OrderNum = 97,
896 - ParentId = system.Id, 896 + ParentId = system.Id.ToString(),
897 IsDeleted = false 897 IsDeleted = false
898 }; 898 };
899 entities.Add(dept); 899 entities.Add(dept);
@@ -905,7 +905,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -905,7 +905,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
905 PermissionCode = "system:dept:query", 905 PermissionCode = "system:dept:query",
906 MenuType = MenuTypeEnum.Component, 906 MenuType = MenuTypeEnum.Component,
907 OrderNum = 100, 907 OrderNum = 100,
908 - ParentId = dept.Id, 908 + ParentId = dept.Id.ToString(),
909 IsDeleted = false 909 IsDeleted = false
910 }; 910 };
911 entities.Add(deptQuery); 911 entities.Add(deptQuery);
@@ -917,7 +917,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -917,7 +917,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
917 PermissionCode = "system:dept:add", 917 PermissionCode = "system:dept:add",
918 MenuType = MenuTypeEnum.Component, 918 MenuType = MenuTypeEnum.Component,
919 OrderNum = 100, 919 OrderNum = 100,
920 - ParentId = dept.Id, 920 + ParentId = dept.Id.ToString(),
921 IsDeleted = false 921 IsDeleted = false
922 }; 922 };
923 entities.Add(deptAdd); 923 entities.Add(deptAdd);
@@ -929,7 +929,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -929,7 +929,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
929 PermissionCode = "system:dept:edit", 929 PermissionCode = "system:dept:edit",
930 MenuType = MenuTypeEnum.Component, 930 MenuType = MenuTypeEnum.Component,
931 OrderNum = 100, 931 OrderNum = 100,
932 - ParentId = dept.Id, 932 + ParentId = dept.Id.ToString(),
933 IsDeleted = false 933 IsDeleted = false
934 }; 934 };
935 entities.Add(deptEdit); 935 entities.Add(deptEdit);
@@ -941,7 +941,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -941,7 +941,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
941 PermissionCode = "system:dept:remove", 941 PermissionCode = "system:dept:remove",
942 MenuType = MenuTypeEnum.Component, 942 MenuType = MenuTypeEnum.Component,
943 OrderNum = 100, 943 OrderNum = 100,
944 - ParentId = dept.Id, 944 + ParentId = dept.Id.ToString(),
945 IsDeleted = false 945 IsDeleted = false
946 }; 946 };
947 entities.Add(deptRemove); 947 entities.Add(deptRemove);
@@ -962,7 +962,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -962,7 +962,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
962 Component = "system/post/index", 962 Component = "system/post/index",
963 MenuIcon = "post", 963 MenuIcon = "post",
964 OrderNum = 96, 964 OrderNum = 96,
965 - ParentId = system.Id, 965 + ParentId = system.Id.ToString(),
966 IsDeleted = false 966 IsDeleted = false
967 }; 967 };
968 entities.Add(post); 968 entities.Add(post);
@@ -974,7 +974,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -974,7 +974,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
974 PermissionCode = "system:post:query", 974 PermissionCode = "system:post:query",
975 MenuType = MenuTypeEnum.Component, 975 MenuType = MenuTypeEnum.Component,
976 OrderNum = 100, 976 OrderNum = 100,
977 - ParentId = post.Id, 977 + ParentId = post.Id.ToString(),
978 IsDeleted = false 978 IsDeleted = false
979 }; 979 };
980 entities.Add(postQuery); 980 entities.Add(postQuery);
@@ -986,7 +986,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -986,7 +986,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
986 PermissionCode = "system:post:add", 986 PermissionCode = "system:post:add",
987 MenuType = MenuTypeEnum.Component, 987 MenuType = MenuTypeEnum.Component,
988 OrderNum = 100, 988 OrderNum = 100,
989 - ParentId = post.Id, 989 + ParentId = post.Id.ToString(),
990 IsDeleted = false 990 IsDeleted = false
991 }; 991 };
992 entities.Add(postAdd); 992 entities.Add(postAdd);
@@ -998,7 +998,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -998,7 +998,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
998 PermissionCode = "system:post:edit", 998 PermissionCode = "system:post:edit",
999 MenuType = MenuTypeEnum.Component, 999 MenuType = MenuTypeEnum.Component,
1000 OrderNum = 100, 1000 OrderNum = 100,
1001 - ParentId = post.Id, 1001 + ParentId = post.Id.ToString(),
1002 IsDeleted = false 1002 IsDeleted = false
1003 }; 1003 };
1004 entities.Add(postEdit); 1004 entities.Add(postEdit);
@@ -1010,7 +1010,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1010,7 +1010,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1010 PermissionCode = "system:post:remove", 1010 PermissionCode = "system:post:remove",
1011 MenuType = MenuTypeEnum.Component, 1011 MenuType = MenuTypeEnum.Component,
1012 OrderNum = 100, 1012 OrderNum = 100,
1013 - ParentId = post.Id, 1013 + ParentId = post.Id.ToString(),
1014 IsDeleted = false 1014 IsDeleted = false
1015 }; 1015 };
1016 entities.Add(postRemove); 1016 entities.Add(postRemove);
@@ -1029,7 +1029,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1029,7 +1029,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1029 Component = "system/dict/index", 1029 Component = "system/dict/index",
1030 MenuIcon = "dict", 1030 MenuIcon = "dict",
1031 OrderNum = 95, 1031 OrderNum = 95,
1032 - ParentId = system.Id, 1032 + ParentId = system.Id.ToString(),
1033 IsDeleted = false 1033 IsDeleted = false
1034 }; 1034 };
1035 entities.Add(dict); 1035 entities.Add(dict);
@@ -1041,7 +1041,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1041,7 +1041,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1041 PermissionCode = "system:dict:query", 1041 PermissionCode = "system:dict:query",
1042 MenuType = MenuTypeEnum.Component, 1042 MenuType = MenuTypeEnum.Component,
1043 OrderNum = 100, 1043 OrderNum = 100,
1044 - ParentId = dict.Id, 1044 + ParentId = dict.Id.ToString(),
1045 IsDeleted = false 1045 IsDeleted = false
1046 }; 1046 };
1047 entities.Add(dictQuery); 1047 entities.Add(dictQuery);
@@ -1053,7 +1053,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1053,7 +1053,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1053 PermissionCode = "system:dict:add", 1053 PermissionCode = "system:dict:add",
1054 MenuType = MenuTypeEnum.Component, 1054 MenuType = MenuTypeEnum.Component,
1055 OrderNum = 100, 1055 OrderNum = 100,
1056 - ParentId = dict.Id, 1056 + ParentId = dict.Id.ToString(),
1057 IsDeleted = false 1057 IsDeleted = false
1058 }; 1058 };
1059 entities.Add(dictAdd); 1059 entities.Add(dictAdd);
@@ -1065,7 +1065,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1065,7 +1065,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1065 PermissionCode = "system:dict:edit", 1065 PermissionCode = "system:dict:edit",
1066 MenuType = MenuTypeEnum.Component, 1066 MenuType = MenuTypeEnum.Component,
1067 OrderNum = 100, 1067 OrderNum = 100,
1068 - ParentId = dict.Id, 1068 + ParentId = dict.Id.ToString(),
1069 IsDeleted = false 1069 IsDeleted = false
1070 }; 1070 };
1071 entities.Add(dictEdit); 1071 entities.Add(dictEdit);
@@ -1077,7 +1077,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1077,7 +1077,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1077 PermissionCode = "system:dict:remove", 1077 PermissionCode = "system:dict:remove",
1078 MenuType = MenuTypeEnum.Component, 1078 MenuType = MenuTypeEnum.Component,
1079 OrderNum = 100, 1079 OrderNum = 100,
1080 - ParentId = dict.Id, 1080 + ParentId = dict.Id.ToString(),
1081 IsDeleted = false 1081 IsDeleted = false
1082 }; 1082 };
1083 entities.Add(dictRemove); 1083 entities.Add(dictRemove);
@@ -1097,7 +1097,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1097,7 +1097,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1097 Component = "system/config/index", 1097 Component = "system/config/index",
1098 MenuIcon = "edit", 1098 MenuIcon = "edit",
1099 OrderNum = 94, 1099 OrderNum = 94,
1100 - ParentId = system.Id, 1100 + ParentId = system.Id.ToString(),
1101 IsDeleted = false 1101 IsDeleted = false
1102 }; 1102 };
1103 entities.Add(config); 1103 entities.Add(config);
@@ -1109,7 +1109,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1109,7 +1109,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1109 PermissionCode = "system:config:query", 1109 PermissionCode = "system:config:query",
1110 MenuType = MenuTypeEnum.Component, 1110 MenuType = MenuTypeEnum.Component,
1111 OrderNum = 100, 1111 OrderNum = 100,
1112 - ParentId = config.Id, 1112 + ParentId = config.Id.ToString(),
1113 IsDeleted = false 1113 IsDeleted = false
1114 }; 1114 };
1115 entities.Add(configQuery); 1115 entities.Add(configQuery);
@@ -1121,7 +1121,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1121,7 +1121,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1121 PermissionCode = "system:config:add", 1121 PermissionCode = "system:config:add",
1122 MenuType = MenuTypeEnum.Component, 1122 MenuType = MenuTypeEnum.Component,
1123 OrderNum = 100, 1123 OrderNum = 100,
1124 - ParentId = config.Id, 1124 + ParentId = config.Id.ToString(),
1125 IsDeleted = false 1125 IsDeleted = false
1126 }; 1126 };
1127 entities.Add(configAdd); 1127 entities.Add(configAdd);
@@ -1133,7 +1133,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1133,7 +1133,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1133 PermissionCode = "system:config:edit", 1133 PermissionCode = "system:config:edit",
1134 MenuType = MenuTypeEnum.Component, 1134 MenuType = MenuTypeEnum.Component,
1135 OrderNum = 100, 1135 OrderNum = 100,
1136 - ParentId = config.Id, 1136 + ParentId = config.Id.ToString(),
1137 IsDeleted = false 1137 IsDeleted = false
1138 }; 1138 };
1139 entities.Add(configEdit); 1139 entities.Add(configEdit);
@@ -1145,7 +1145,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1145,7 +1145,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1145 PermissionCode = "system:config:remove", 1145 PermissionCode = "system:config:remove",
1146 MenuType = MenuTypeEnum.Component, 1146 MenuType = MenuTypeEnum.Component,
1147 OrderNum = 100, 1147 OrderNum = 100,
1148 - ParentId = config.Id, 1148 + ParentId = config.Id.ToString(),
1149 IsDeleted = false 1149 IsDeleted = false
1150 }; 1150 };
1151 entities.Add(configRemove); 1151 entities.Add(configRemove);
@@ -1167,7 +1167,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1167,7 +1167,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1167 Component = "system/notice/index", 1167 Component = "system/notice/index",
1168 MenuIcon = "message", 1168 MenuIcon = "message",
1169 OrderNum = 93, 1169 OrderNum = 93,
1170 - ParentId = system.Id, 1170 + ParentId = system.Id.ToString(),
1171 IsDeleted = false 1171 IsDeleted = false
1172 }; 1172 };
1173 entities.Add(notice); 1173 entities.Add(notice);
@@ -1179,7 +1179,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1179,7 +1179,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1179 PermissionCode = "system:notice:query", 1179 PermissionCode = "system:notice:query",
1180 MenuType = MenuTypeEnum.Component, 1180 MenuType = MenuTypeEnum.Component,
1181 OrderNum = 100, 1181 OrderNum = 100,
1182 - ParentId = notice.Id, 1182 + ParentId = notice.Id.ToString(),
1183 IsDeleted = false 1183 IsDeleted = false
1184 }; 1184 };
1185 entities.Add(noticeQuery); 1185 entities.Add(noticeQuery);
@@ -1191,7 +1191,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1191,7 +1191,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1191 PermissionCode = "system:notice:add", 1191 PermissionCode = "system:notice:add",
1192 MenuType = MenuTypeEnum.Component, 1192 MenuType = MenuTypeEnum.Component,
1193 OrderNum = 100, 1193 OrderNum = 100,
1194 - ParentId = notice.Id, 1194 + ParentId = notice.Id.ToString(),
1195 IsDeleted = false 1195 IsDeleted = false
1196 }; 1196 };
1197 entities.Add(noticeAdd); 1197 entities.Add(noticeAdd);
@@ -1203,7 +1203,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1203,7 +1203,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1203 PermissionCode = "system:notice:edit", 1203 PermissionCode = "system:notice:edit",
1204 MenuType = MenuTypeEnum.Component, 1204 MenuType = MenuTypeEnum.Component,
1205 OrderNum = 100, 1205 OrderNum = 100,
1206 - ParentId = notice.Id, 1206 + ParentId = notice.Id.ToString(),
1207 IsDeleted = false 1207 IsDeleted = false
1208 }; 1208 };
1209 entities.Add(noticeEdit); 1209 entities.Add(noticeEdit);
@@ -1215,7 +1215,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1215,7 +1215,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1215 PermissionCode = "system:notice:remove", 1215 PermissionCode = "system:notice:remove",
1216 MenuType = MenuTypeEnum.Component, 1216 MenuType = MenuTypeEnum.Component,
1217 OrderNum = 100, 1217 OrderNum = 100,
1218 - ParentId = notice.Id, 1218 + ParentId = notice.Id.ToString(),
1219 IsDeleted = false 1219 IsDeleted = false
1220 }; 1220 };
1221 entities.Add(noticeRemove); 1221 entities.Add(noticeRemove);
@@ -1233,7 +1233,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1233,7 +1233,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1233 IsLink = false, 1233 IsLink = false,
1234 MenuIcon = "log", 1234 MenuIcon = "log",
1235 OrderNum = 92, 1235 OrderNum = 92,
1236 - ParentId = system.Id, 1236 + ParentId = system.Id.ToString(),
1237 IsDeleted = false 1237 IsDeleted = false
1238 }; 1238 };
1239 entities.Add(log); 1239 entities.Add(log);
@@ -1252,7 +1252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1252,7 +1252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1252 Component = "monitor/operlog/index", 1252 Component = "monitor/operlog/index",
1253 MenuIcon = "form", 1253 MenuIcon = "form",
1254 OrderNum = 100, 1254 OrderNum = 100,
1255 - ParentId = log.Id, 1255 + ParentId = log.Id.ToString(),
1256 IsDeleted = false 1256 IsDeleted = false
1257 }; 1257 };
1258 entities.Add(operationLog); 1258 entities.Add(operationLog);
@@ -1264,7 +1264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1264,7 +1264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1264 PermissionCode = "monitor:operlog:query", 1264 PermissionCode = "monitor:operlog:query",
1265 MenuType = MenuTypeEnum.Component, 1265 MenuType = MenuTypeEnum.Component,
1266 OrderNum = 100, 1266 OrderNum = 100,
1267 - ParentId = operationLog.Id, 1267 + ParentId = operationLog.Id.ToString(),
1268 IsDeleted = false 1268 IsDeleted = false
1269 }; 1269 };
1270 entities.Add(operationLogQuery); 1270 entities.Add(operationLogQuery);
@@ -1276,7 +1276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1276,7 +1276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1276 PermissionCode = "monitor:operlog:remove", 1276 PermissionCode = "monitor:operlog:remove",
1277 MenuType = MenuTypeEnum.Component, 1277 MenuType = MenuTypeEnum.Component,
1278 OrderNum = 100, 1278 OrderNum = 100,
1279 - ParentId = operationLog.Id, 1279 + ParentId = operationLog.Id.ToString(),
1280 IsDeleted = false 1280 IsDeleted = false
1281 }; 1281 };
1282 entities.Add(operationLogRemove); 1282 entities.Add(operationLogRemove);
@@ -1296,7 +1296,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1296,7 +1296,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1296 Component = "monitor/logininfor/index", 1296 Component = "monitor/logininfor/index",
1297 MenuIcon = "logininfor", 1297 MenuIcon = "logininfor",
1298 OrderNum = 100, 1298 OrderNum = 100,
1299 - ParentId = log.Id, 1299 + ParentId = log.Id.ToString(),
1300 IsDeleted = false 1300 IsDeleted = false
1301 }; 1301 };
1302 entities.Add(loginLog); 1302 entities.Add(loginLog);
@@ -1308,7 +1308,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1308,7 +1308,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1308 PermissionCode = "monitor:logininfor:query", 1308 PermissionCode = "monitor:logininfor:query",
1309 MenuType = MenuTypeEnum.Component, 1309 MenuType = MenuTypeEnum.Component,
1310 OrderNum = 100, 1310 OrderNum = 100,
1311 - ParentId = loginLog.Id, 1311 + ParentId = loginLog.Id.ToString(),
1312 IsDeleted = false 1312 IsDeleted = false
1313 }; 1313 };
1314 entities.Add(loginLogQuery); 1314 entities.Add(loginLogQuery);
@@ -1320,7 +1320,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -1320,7 +1320,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1320 PermissionCode = "monitor:logininfor:remove", 1320 PermissionCode = "monitor:logininfor:remove",
1321 MenuType = MenuTypeEnum.Component, 1321 MenuType = MenuTypeEnum.Component,
1322 OrderNum = 100, 1322 OrderNum = 100,
1323 - ParentId = loginLog.Id, 1323 + ParentId = loginLog.Id.ToString(),
1324 IsDeleted = false 1324 IsDeleted = false
1325 }; 1325 };
1326 entities.Add(loginLogRemove); 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 using Volo.Abp.DependencyInjection; 2 using Volo.Abp.DependencyInjection;
3 using Volo.Abp.Guids; 3 using Volo.Abp.Guids;
4 using Yi.Framework.Rbac.Domain.Entities; 4 using Yi.Framework.Rbac.Domain.Entities;
@@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
88 // Component = "code/field/index", 88 // Component = "code/field/index",
89 // MenuIcon = "tabler:file-code", 89 // MenuIcon = "tabler:file-code",
90 // OrderNum = 99, 90 // OrderNum = 99,
91 - // ParentId = code.Id, 91 + // ParentId = code.Id.ToString(),
92 // IsDeleted = false 92 // IsDeleted = false
93 // }; 93 // };
94 // entities.Add(field); 94 // entities.Add(field);
@@ -251,7 +251,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -251,7 +251,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
251 Component = "system/tenant/index", 251 Component = "system/tenant/index",
252 MenuIcon = "tabler:users", 252 MenuIcon = "tabler:users",
253 OrderNum = 101, 253 OrderNum = 101,
254 - ParentId = system.Id, 254 + ParentId = system.Id.ToString(),
255 IsDeleted = false 255 IsDeleted = false
256 }; 256 };
257 entities.Add(tenant); 257 entities.Add(tenant);
@@ -263,7 +263,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -263,7 +263,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
263 PermissionCode = "system:tenant:query", 263 PermissionCode = "system:tenant:query",
264 MenuType = MenuTypeEnum.Component, 264 MenuType = MenuTypeEnum.Component,
265 OrderNum = 100, 265 OrderNum = 100,
266 - ParentId = tenant.Id, 266 + ParentId = tenant.Id.ToString(),
267 IsDeleted = false 267 IsDeleted = false
268 }; 268 };
269 entities.Add(tenantQuery); 269 entities.Add(tenantQuery);
@@ -275,7 +275,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -275,7 +275,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
275 PermissionCode = "system:tenant:add", 275 PermissionCode = "system:tenant:add",
276 MenuType = MenuTypeEnum.Component, 276 MenuType = MenuTypeEnum.Component,
277 OrderNum = 100, 277 OrderNum = 100,
278 - ParentId = tenant.Id, 278 + ParentId = tenant.Id.ToString(),
279 IsDeleted = false 279 IsDeleted = false
280 }; 280 };
281 entities.Add(tenantAdd); 281 entities.Add(tenantAdd);
@@ -287,7 +287,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -287,7 +287,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
287 PermissionCode = "system:tenant:edit", 287 PermissionCode = "system:tenant:edit",
288 MenuType = MenuTypeEnum.Component, 288 MenuType = MenuTypeEnum.Component,
289 OrderNum = 100, 289 OrderNum = 100,
290 - ParentId = tenant.Id, 290 + ParentId = tenant.Id.ToString(),
291 IsDeleted = false 291 IsDeleted = false
292 }; 292 };
293 entities.Add(tenantEdit); 293 entities.Add(tenantEdit);
@@ -299,7 +299,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -299,7 +299,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
299 PermissionCode = "system:tenant:remove", 299 PermissionCode = "system:tenant:remove",
300 MenuType = MenuTypeEnum.Component, 300 MenuType = MenuTypeEnum.Component,
301 OrderNum = 100, 301 OrderNum = 100,
302 - ParentId = tenant.Id, 302 + ParentId = tenant.Id.ToString(),
303 IsDeleted = false 303 IsDeleted = false
304 }; 304 };
305 entities.Add(tenantRemove); 305 entities.Add(tenantRemove);
@@ -318,7 +318,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -318,7 +318,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
318 Component = "system/user/index", 318 Component = "system/user/index",
319 MenuIcon = "tabler:user", 319 MenuIcon = "tabler:user",
320 OrderNum = 100, 320 OrderNum = 100,
321 - ParentId = system.Id, 321 + ParentId = system.Id.ToString(),
322 IsDeleted = false 322 IsDeleted = false
323 }; 323 };
324 entities.Add(user); 324 entities.Add(user);
@@ -330,7 +330,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -330,7 +330,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
330 PermissionCode = "system:user:query", 330 PermissionCode = "system:user:query",
331 MenuType = MenuTypeEnum.Component, 331 MenuType = MenuTypeEnum.Component,
332 OrderNum = 100, 332 OrderNum = 100,
333 - ParentId = user.Id, 333 + ParentId = user.Id.ToString(),
334 IsDeleted = false 334 IsDeleted = false
335 }; 335 };
336 entities.Add(userQuery); 336 entities.Add(userQuery);
@@ -342,7 +342,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -342,7 +342,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
342 PermissionCode = "system:user:add", 342 PermissionCode = "system:user:add",
343 MenuType = MenuTypeEnum.Component, 343 MenuType = MenuTypeEnum.Component,
344 OrderNum = 100, 344 OrderNum = 100,
345 - ParentId = user.Id, 345 + ParentId = user.Id.ToString(),
346 IsDeleted = false 346 IsDeleted = false
347 }; 347 };
348 entities.Add(userAdd); 348 entities.Add(userAdd);
@@ -354,7 +354,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -354,7 +354,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
354 PermissionCode = "system:user:edit", 354 PermissionCode = "system:user:edit",
355 MenuType = MenuTypeEnum.Component, 355 MenuType = MenuTypeEnum.Component,
356 OrderNum = 100, 356 OrderNum = 100,
357 - ParentId = user.Id, 357 + ParentId = user.Id.ToString(),
358 IsDeleted = false 358 IsDeleted = false
359 }; 359 };
360 entities.Add(userEdit); 360 entities.Add(userEdit);
@@ -366,7 +366,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -366,7 +366,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
366 PermissionCode = "system:user:remove", 366 PermissionCode = "system:user:remove",
367 MenuType = MenuTypeEnum.Component, 367 MenuType = MenuTypeEnum.Component,
368 OrderNum = 100, 368 OrderNum = 100,
369 - ParentId = user.Id, 369 + ParentId = user.Id.ToString(),
370 IsDeleted = false 370 IsDeleted = false
371 }; 371 };
372 entities.Add(userRemove); 372 entities.Add(userRemove);
@@ -379,7 +379,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -379,7 +379,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
379 PermissionCode = "system:user:resetPwd", 379 PermissionCode = "system:user:resetPwd",
380 MenuType = MenuTypeEnum.Component, 380 MenuType = MenuTypeEnum.Component,
381 OrderNum = 100, 381 OrderNum = 100,
382 - ParentId = user.Id, 382 + ParentId = user.Id.ToString(),
383 IsDeleted = false 383 IsDeleted = false
384 }; 384 };
385 entities.Add(userResetPwd); 385 entities.Add(userResetPwd);
@@ -399,7 +399,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -399,7 +399,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
399 Component = "system/role/index", 399 Component = "system/role/index",
400 MenuIcon = "eos-icons:role-binding-outlined", 400 MenuIcon = "eos-icons:role-binding-outlined",
401 OrderNum = 99, 401 OrderNum = 99,
402 - ParentId = system.Id, 402 + ParentId = system.Id.ToString(),
403 IsDeleted = false 403 IsDeleted = false
404 }; 404 };
405 entities.Add(role); 405 entities.Add(role);
@@ -411,7 +411,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -411,7 +411,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
411 PermissionCode = "system:role:query", 411 PermissionCode = "system:role:query",
412 MenuType = MenuTypeEnum.Component, 412 MenuType = MenuTypeEnum.Component,
413 OrderNum = 100, 413 OrderNum = 100,
414 - ParentId = role.Id, 414 + ParentId = role.Id.ToString(),
415 IsDeleted = false 415 IsDeleted = false
416 }; 416 };
417 entities.Add(roleQuery); 417 entities.Add(roleQuery);
@@ -423,7 +423,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -423,7 +423,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
423 PermissionCode = "system:role:add", 423 PermissionCode = "system:role:add",
424 MenuType = MenuTypeEnum.Component, 424 MenuType = MenuTypeEnum.Component,
425 OrderNum = 100, 425 OrderNum = 100,
426 - ParentId = role.Id, 426 + ParentId = role.Id.ToString(),
427 IsDeleted = false 427 IsDeleted = false
428 }; 428 };
429 entities.Add(roleAdd); 429 entities.Add(roleAdd);
@@ -435,7 +435,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -435,7 +435,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
435 PermissionCode = "system:role:edit", 435 PermissionCode = "system:role:edit",
436 MenuType = MenuTypeEnum.Component, 436 MenuType = MenuTypeEnum.Component,
437 OrderNum = 100, 437 OrderNum = 100,
438 - ParentId = role.Id, 438 + ParentId = role.Id.ToString(),
439 IsDeleted = false 439 IsDeleted = false
440 }; 440 };
441 entities.Add(roleEdit); 441 entities.Add(roleEdit);
@@ -447,7 +447,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -447,7 +447,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
447 PermissionCode = "system:role:remove", 447 PermissionCode = "system:role:remove",
448 MenuType = MenuTypeEnum.Component, 448 MenuType = MenuTypeEnum.Component,
449 OrderNum = 100, 449 OrderNum = 100,
450 - ParentId = role.Id, 450 + ParentId = role.Id.ToString(),
451 IsDeleted = false 451 IsDeleted = false
452 }; 452 };
453 entities.Add(roleRemove); 453 entities.Add(roleRemove);
@@ -466,7 +466,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -466,7 +466,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
466 MenuIcon = "tabler:user-shield", 466 MenuIcon = "tabler:user-shield",
467 OrderNum = 15, 467 OrderNum = 15,
468 IsDeleted = false, 468 IsDeleted = false,
469 - ParentId = system.Id 469 + ParentId = system.Id.ToString()
470 }; 470 };
471 entities.Add(roleAuthUser); 471 entities.Add(roleAuthUser);
472 472
@@ -485,7 +485,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -485,7 +485,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
485 Component = "system/menu/index", 485 Component = "system/menu/index",
486 MenuIcon = "ic:sharp-menu", 486 MenuIcon = "ic:sharp-menu",
487 OrderNum = 98, 487 OrderNum = 98,
488 - ParentId = system.Id, 488 + ParentId = system.Id.ToString(),
489 IsDeleted = false 489 IsDeleted = false
490 }; 490 };
491 entities.Add(menu); 491 entities.Add(menu);
@@ -497,7 +497,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -497,7 +497,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
497 PermissionCode = "system:menu:query", 497 PermissionCode = "system:menu:query",
498 MenuType = MenuTypeEnum.Component, 498 MenuType = MenuTypeEnum.Component,
499 OrderNum = 100, 499 OrderNum = 100,
500 - ParentId = menu.Id, 500 + ParentId = menu.Id.ToString(),
501 IsDeleted = false 501 IsDeleted = false
502 }; 502 };
503 entities.Add(menuQuery); 503 entities.Add(menuQuery);
@@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
509 PermissionCode = "system:menu:add", 509 PermissionCode = "system:menu:add",
510 MenuType = MenuTypeEnum.Component, 510 MenuType = MenuTypeEnum.Component,
511 OrderNum = 100, 511 OrderNum = 100,
512 - ParentId = menu.Id, 512 + ParentId = menu.Id.ToString(),
513 IsDeleted = false 513 IsDeleted = false
514 }; 514 };
515 entities.Add(menuAdd); 515 entities.Add(menuAdd);
@@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
521 PermissionCode = "system:menu:edit", 521 PermissionCode = "system:menu:edit",
522 MenuType = MenuTypeEnum.Component, 522 MenuType = MenuTypeEnum.Component,
523 OrderNum = 100, 523 OrderNum = 100,
524 - ParentId = menu.Id, 524 + ParentId = menu.Id.ToString(),
525 IsDeleted = false 525 IsDeleted = false
526 }; 526 };
527 entities.Add(menuEdit); 527 entities.Add(menuEdit);
@@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
533 PermissionCode = "system:menu:remove", 533 PermissionCode = "system:menu:remove",
534 MenuType = MenuTypeEnum.Component, 534 MenuType = MenuTypeEnum.Component,
535 OrderNum = 100, 535 OrderNum = 100,
536 - ParentId = menu.Id, 536 + ParentId = menu.Id.ToString(),
537 IsDeleted = false 537 IsDeleted = false
538 }; 538 };
539 entities.Add(menuRemove); 539 entities.Add(menuRemove);
@@ -552,7 +552,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -552,7 +552,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
552 Component = "system/dept/index", 552 Component = "system/dept/index",
553 MenuIcon = "mingcute:department-line", 553 MenuIcon = "mingcute:department-line",
554 OrderNum = 97, 554 OrderNum = 97,
555 - ParentId = system.Id, 555 + ParentId = system.Id.ToString(),
556 IsDeleted = false 556 IsDeleted = false
557 }; 557 };
558 entities.Add(dept); 558 entities.Add(dept);
@@ -564,7 +564,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -564,7 +564,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
564 PermissionCode = "system:dept:query", 564 PermissionCode = "system:dept:query",
565 MenuType = MenuTypeEnum.Component, 565 MenuType = MenuTypeEnum.Component,
566 OrderNum = 100, 566 OrderNum = 100,
567 - ParentId = dept.Id, 567 + ParentId = dept.Id.ToString(),
568 IsDeleted = false 568 IsDeleted = false
569 }; 569 };
570 entities.Add(deptQuery); 570 entities.Add(deptQuery);
@@ -576,7 +576,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -576,7 +576,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
576 PermissionCode = "system:dept:add", 576 PermissionCode = "system:dept:add",
577 MenuType = MenuTypeEnum.Component, 577 MenuType = MenuTypeEnum.Component,
578 OrderNum = 100, 578 OrderNum = 100,
579 - ParentId = dept.Id, 579 + ParentId = dept.Id.ToString(),
580 IsDeleted = false 580 IsDeleted = false
581 }; 581 };
582 entities.Add(deptAdd); 582 entities.Add(deptAdd);
@@ -588,7 +588,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -588,7 +588,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
588 PermissionCode = "system:dept:edit", 588 PermissionCode = "system:dept:edit",
589 MenuType = MenuTypeEnum.Component, 589 MenuType = MenuTypeEnum.Component,
590 OrderNum = 100, 590 OrderNum = 100,
591 - ParentId = dept.Id, 591 + ParentId = dept.Id.ToString(),
592 IsDeleted = false 592 IsDeleted = false
593 }; 593 };
594 entities.Add(deptEdit); 594 entities.Add(deptEdit);
@@ -600,7 +600,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -600,7 +600,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
600 PermissionCode = "system:dept:remove", 600 PermissionCode = "system:dept:remove",
601 MenuType = MenuTypeEnum.Component, 601 MenuType = MenuTypeEnum.Component,
602 OrderNum = 100, 602 OrderNum = 100,
603 - ParentId = dept.Id, 603 + ParentId = dept.Id.ToString(),
604 IsDeleted = false 604 IsDeleted = false
605 }; 605 };
606 entities.Add(deptRemove); 606 entities.Add(deptRemove);
@@ -621,7 +621,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -621,7 +621,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
621 Component = "system/post/index", 621 Component = "system/post/index",
622 MenuIcon = "tabler:user-star", 622 MenuIcon = "tabler:user-star",
623 OrderNum = 96, 623 OrderNum = 96,
624 - ParentId = system.Id, 624 + ParentId = system.Id.ToString(),
625 IsDeleted = false 625 IsDeleted = false
626 }; 626 };
627 entities.Add(post); 627 entities.Add(post);
@@ -633,7 +633,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -633,7 +633,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
633 PermissionCode = "system:post:query", 633 PermissionCode = "system:post:query",
634 MenuType = MenuTypeEnum.Component, 634 MenuType = MenuTypeEnum.Component,
635 OrderNum = 100, 635 OrderNum = 100,
636 - ParentId = post.Id, 636 + ParentId = post.Id.ToString(),
637 IsDeleted = false 637 IsDeleted = false
638 }; 638 };
639 entities.Add(postQuery); 639 entities.Add(postQuery);
@@ -645,7 +645,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -645,7 +645,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
645 PermissionCode = "system:post:add", 645 PermissionCode = "system:post:add",
646 MenuType = MenuTypeEnum.Component, 646 MenuType = MenuTypeEnum.Component,
647 OrderNum = 100, 647 OrderNum = 100,
648 - ParentId = post.Id, 648 + ParentId = post.Id.ToString(),
649 IsDeleted = false 649 IsDeleted = false
650 }; 650 };
651 entities.Add(postAdd); 651 entities.Add(postAdd);
@@ -657,7 +657,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -657,7 +657,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
657 PermissionCode = "system:post:edit", 657 PermissionCode = "system:post:edit",
658 MenuType = MenuTypeEnum.Component, 658 MenuType = MenuTypeEnum.Component,
659 OrderNum = 100, 659 OrderNum = 100,
660 - ParentId = post.Id, 660 + ParentId = post.Id.ToString(),
661 IsDeleted = false 661 IsDeleted = false
662 }; 662 };
663 entities.Add(postEdit); 663 entities.Add(postEdit);
@@ -669,7 +669,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -669,7 +669,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
669 PermissionCode = "system:post:remove", 669 PermissionCode = "system:post:remove",
670 MenuType = MenuTypeEnum.Component, 670 MenuType = MenuTypeEnum.Component,
671 OrderNum = 100, 671 OrderNum = 100,
672 - ParentId = post.Id, 672 + ParentId = post.Id.ToString(),
673 IsDeleted = false 673 IsDeleted = false
674 }; 674 };
675 entities.Add(postRemove); 675 entities.Add(postRemove);
@@ -688,7 +688,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -688,7 +688,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
688 Component = "system/dict/index", 688 Component = "system/dict/index",
689 MenuIcon = "fluent-mdl2:dictionary", 689 MenuIcon = "fluent-mdl2:dictionary",
690 OrderNum = 95, 690 OrderNum = 95,
691 - ParentId = system.Id, 691 + ParentId = system.Id.ToString(),
692 IsDeleted = false 692 IsDeleted = false
693 }; 693 };
694 entities.Add(dict); 694 entities.Add(dict);
@@ -700,7 +700,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -700,7 +700,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
700 PermissionCode = "system:dict:query", 700 PermissionCode = "system:dict:query",
701 MenuType = MenuTypeEnum.Component, 701 MenuType = MenuTypeEnum.Component,
702 OrderNum = 100, 702 OrderNum = 100,
703 - ParentId = dict.Id, 703 + ParentId = dict.Id.ToString(),
704 IsDeleted = false 704 IsDeleted = false
705 }; 705 };
706 entities.Add(dictQuery); 706 entities.Add(dictQuery);
@@ -712,7 +712,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -712,7 +712,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
712 PermissionCode = "system:dict:add", 712 PermissionCode = "system:dict:add",
713 MenuType = MenuTypeEnum.Component, 713 MenuType = MenuTypeEnum.Component,
714 OrderNum = 100, 714 OrderNum = 100,
715 - ParentId = dict.Id, 715 + ParentId = dict.Id.ToString(),
716 IsDeleted = false 716 IsDeleted = false
717 }; 717 };
718 entities.Add(dictAdd); 718 entities.Add(dictAdd);
@@ -724,7 +724,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -724,7 +724,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
724 PermissionCode = "system:dict:edit", 724 PermissionCode = "system:dict:edit",
725 MenuType = MenuTypeEnum.Component, 725 MenuType = MenuTypeEnum.Component,
726 OrderNum = 100, 726 OrderNum = 100,
727 - ParentId = dict.Id, 727 + ParentId = dict.Id.ToString(),
728 IsDeleted = false 728 IsDeleted = false
729 }; 729 };
730 entities.Add(dictEdit); 730 entities.Add(dictEdit);
@@ -736,7 +736,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -736,7 +736,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
736 PermissionCode = "system:dict:remove", 736 PermissionCode = "system:dict:remove",
737 MenuType = MenuTypeEnum.Component, 737 MenuType = MenuTypeEnum.Component,
738 OrderNum = 100, 738 OrderNum = 100,
739 - ParentId = dict.Id, 739 + ParentId = dict.Id.ToString(),
740 IsDeleted = false 740 IsDeleted = false
741 }; 741 };
742 entities.Add(dictRemove); 742 entities.Add(dictRemove);
@@ -756,7 +756,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -756,7 +756,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
756 Component = "system/config/index", 756 Component = "system/config/index",
757 MenuIcon = "ant-design:setting-outlined", 757 MenuIcon = "ant-design:setting-outlined",
758 OrderNum = 94, 758 OrderNum = 94,
759 - ParentId = system.Id, 759 + ParentId = system.Id.ToString(),
760 IsDeleted = false 760 IsDeleted = false
761 }; 761 };
762 entities.Add(config); 762 entities.Add(config);
@@ -768,7 +768,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -768,7 +768,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
768 PermissionCode = "system:config:query", 768 PermissionCode = "system:config:query",
769 MenuType = MenuTypeEnum.Component, 769 MenuType = MenuTypeEnum.Component,
770 OrderNum = 100, 770 OrderNum = 100,
771 - ParentId = config.Id, 771 + ParentId = config.Id.ToString(),
772 IsDeleted = false 772 IsDeleted = false
773 }; 773 };
774 entities.Add(configQuery); 774 entities.Add(configQuery);
@@ -780,7 +780,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -780,7 +780,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
780 PermissionCode = "system:config:add", 780 PermissionCode = "system:config:add",
781 MenuType = MenuTypeEnum.Component, 781 MenuType = MenuTypeEnum.Component,
782 OrderNum = 100, 782 OrderNum = 100,
783 - ParentId = config.Id, 783 + ParentId = config.Id.ToString(),
784 IsDeleted = false 784 IsDeleted = false
785 }; 785 };
786 entities.Add(configAdd); 786 entities.Add(configAdd);
@@ -792,7 +792,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -792,7 +792,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
792 PermissionCode = "system:config:edit", 792 PermissionCode = "system:config:edit",
793 MenuType = MenuTypeEnum.Component, 793 MenuType = MenuTypeEnum.Component,
794 OrderNum = 100, 794 OrderNum = 100,
795 - ParentId = config.Id, 795 + ParentId = config.Id.ToString(),
796 IsDeleted = false 796 IsDeleted = false
797 }; 797 };
798 entities.Add(configEdit); 798 entities.Add(configEdit);
@@ -804,7 +804,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -804,7 +804,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
804 PermissionCode = "system:config:remove", 804 PermissionCode = "system:config:remove",
805 MenuType = MenuTypeEnum.Component, 805 MenuType = MenuTypeEnum.Component,
806 OrderNum = 100, 806 OrderNum = 100,
807 - ParentId = config.Id, 807 + ParentId = config.Id.ToString(),
808 IsDeleted = false 808 IsDeleted = false
809 }; 809 };
810 entities.Add(configRemove); 810 entities.Add(configRemove);
@@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
826 Component = "system/notice/index", 826 Component = "system/notice/index",
827 MenuIcon = "fe:notice-push", 827 MenuIcon = "fe:notice-push",
828 OrderNum = 93, 828 OrderNum = 93,
829 - ParentId = system.Id, 829 + ParentId = system.Id.ToString(),
830 IsDeleted = false 830 IsDeleted = false
831 }; 831 };
832 entities.Add(notice); 832 entities.Add(notice);
@@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
838 PermissionCode = "system:notice:query", 838 PermissionCode = "system:notice:query",
839 MenuType = MenuTypeEnum.Component, 839 MenuType = MenuTypeEnum.Component,
840 OrderNum = 100, 840 OrderNum = 100,
841 - ParentId = notice.Id, 841 + ParentId = notice.Id.ToString(),
842 IsDeleted = false 842 IsDeleted = false
843 }; 843 };
844 entities.Add(noticeQuery); 844 entities.Add(noticeQuery);
@@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
850 PermissionCode = "system:notice:add", 850 PermissionCode = "system:notice:add",
851 MenuType = MenuTypeEnum.Component, 851 MenuType = MenuTypeEnum.Component,
852 OrderNum = 100, 852 OrderNum = 100,
853 - ParentId = notice.Id, 853 + ParentId = notice.Id.ToString(),
854 IsDeleted = false 854 IsDeleted = false
855 }; 855 };
856 entities.Add(noticeAdd); 856 entities.Add(noticeAdd);
@@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
862 PermissionCode = "system:notice:edit", 862 PermissionCode = "system:notice:edit",
863 MenuType = MenuTypeEnum.Component, 863 MenuType = MenuTypeEnum.Component,
864 OrderNum = 100, 864 OrderNum = 100,
865 - ParentId = notice.Id, 865 + ParentId = notice.Id.ToString(),
866 IsDeleted = false 866 IsDeleted = false
867 }; 867 };
868 entities.Add(noticeEdit); 868 entities.Add(noticeEdit);
@@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
874 PermissionCode = "system:notice:remove", 874 PermissionCode = "system:notice:remove",
875 MenuType = MenuTypeEnum.Component, 875 MenuType = MenuTypeEnum.Component,
876 OrderNum = 100, 876 OrderNum = 100,
877 - ParentId = notice.Id, 877 + ParentId = notice.Id.ToString(),
878 IsDeleted = false 878 IsDeleted = false
879 }; 879 };
880 entities.Add(noticeRemove); 880 entities.Add(noticeRemove);
@@ -892,7 +892,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -892,7 +892,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
892 IsLink = false, 892 IsLink = false,
893 MenuIcon = "material-symbols:logo-dev-outline", 893 MenuIcon = "material-symbols:logo-dev-outline",
894 OrderNum = 92, 894 OrderNum = 92,
895 - ParentId = system.Id, 895 + ParentId = system.Id.ToString(),
896 IsDeleted = false 896 IsDeleted = false
897 }; 897 };
898 entities.Add(log); 898 entities.Add(log);
@@ -911,7 +911,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -911,7 +911,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
911 Component = "monitor/operlog/index", 911 Component = "monitor/operlog/index",
912 MenuIcon = "tabler:align-box-right-middle", 912 MenuIcon = "tabler:align-box-right-middle",
913 OrderNum = 100, 913 OrderNum = 100,
914 - ParentId = log.Id, 914 + ParentId = log.Id.ToString(),
915 IsDeleted = false 915 IsDeleted = false
916 }; 916 };
917 entities.Add(operationLog); 917 entities.Add(operationLog);
@@ -923,7 +923,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -923,7 +923,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
923 PermissionCode = "monitor:operlog:query", 923 PermissionCode = "monitor:operlog:query",
924 MenuType = MenuTypeEnum.Component, 924 MenuType = MenuTypeEnum.Component,
925 OrderNum = 100, 925 OrderNum = 100,
926 - ParentId = operationLog.Id, 926 + ParentId = operationLog.Id.ToString(),
927 IsDeleted = false 927 IsDeleted = false
928 }; 928 };
929 entities.Add(operationLogQuery); 929 entities.Add(operationLogQuery);
@@ -935,7 +935,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -935,7 +935,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
935 PermissionCode = "monitor:operlog:remove", 935 PermissionCode = "monitor:operlog:remove",
936 MenuType = MenuTypeEnum.Component, 936 MenuType = MenuTypeEnum.Component,
937 OrderNum = 100, 937 OrderNum = 100,
938 - ParentId = operationLog.Id, 938 + ParentId = operationLog.Id.ToString(),
939 IsDeleted = false 939 IsDeleted = false
940 }; 940 };
941 entities.Add(operationLogRemove); 941 entities.Add(operationLogRemove);
@@ -955,7 +955,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -955,7 +955,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
955 Component = "monitor/logininfor/index", 955 Component = "monitor/logininfor/index",
956 MenuIcon = "tabler:align-box-right-middle", 956 MenuIcon = "tabler:align-box-right-middle",
957 OrderNum = 100, 957 OrderNum = 100,
958 - ParentId = log.Id, 958 + ParentId = log.Id.ToString(),
959 IsDeleted = false 959 IsDeleted = false
960 }; 960 };
961 entities.Add(loginLog); 961 entities.Add(loginLog);
@@ -967,7 +967,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -967,7 +967,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
967 PermissionCode = "monitor:logininfor:query", 967 PermissionCode = "monitor:logininfor:query",
968 MenuType = MenuTypeEnum.Component, 968 MenuType = MenuTypeEnum.Component,
969 OrderNum = 100, 969 OrderNum = 100,
970 - ParentId = loginLog.Id, 970 + ParentId = loginLog.Id.ToString(),
971 IsDeleted = false 971 IsDeleted = false
972 }; 972 };
973 entities.Add(loginLogQuery); 973 entities.Add(loginLogQuery);
@@ -979,7 +979,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds @@ -979,7 +979,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
979 PermissionCode = "monitor:logininfor:remove", 979 PermissionCode = "monitor:logininfor:remove",
980 MenuType = MenuTypeEnum.Component, 980 MenuType = MenuTypeEnum.Component,
981 OrderNum = 100, 981 OrderNum = 100,
982 - ParentId = loginLog.Id, 982 + ParentId = loginLog.Id.ToString(),
983 IsDeleted = false 983 IsDeleted = false
984 }; 984 };
985 entities.Add(loginLogRemove); 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,7 +8,9 @@
8 - **接口前缀**:宿主统一前缀为 `/api/app` 8 - **接口前缀**:宿主统一前缀为 `/api/app`
9 - **分类表**:`fl_product_category` 9 - **分类表**:`fl_product_category`
10 - **关联字段**:`fl_product.category_id` → `fl_product_category.id` 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 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 15 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。
14 16
@@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi... @@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi...
57 | `id` | string | 主键 | 59 | `id` | string | 主键 |
58 | `categoryCode` | string | 类别编码 | 60 | `categoryCode` | string | 类别编码 |
59 | `categoryName` | string | 类别名称 | 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 | `state` | boolean | 是否启用 | 66 | `state` | boolean | 是否启用 |
62 | `orderNum` | number | 排序 | 67 | `orderNum` | number | 排序 |
63 | `lastEdited` | string | 最后编辑时间 | 68 | `lastEdited` | string | 最后编辑时间 |
@@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi... @@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi...
75 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", 80 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
76 "categoryCode": "CAT_PREP", 81 "categoryCode": "CAT_PREP",
77 "categoryName": "Prep", 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 "state": true, 87 "state": true,
80 "orderNum": 100, 88 "orderNum": 100,
81 "lastEdited": "2026-03-25 12:30:10" 89 "lastEdited": "2026-03-25 12:30:10"
@@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi... @@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi...
108 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", 116 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
109 "categoryCode": "CAT_PREP", 117 "categoryCode": "CAT_PREP",
110 "categoryName": "Prep", 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 "state": true, 124 "state": true,
113 "orderNum": 100 125 "orderNum": 100
114 } 126 }
@@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi... @@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi...
130 |------|------|------|------| 142 |------|------|------|------|
131 | `categoryCode` | string | 是 | 类别编码(唯一) | 143 | `categoryCode` | string | 是 | 类别编码(唯一) |
132 | `categoryName` | string | 是 | 类别名称(唯一) | 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 | `state` | boolean | 否 | 是否启用(默认 true) | 150 | `state` | boolean | 否 | 是否启用(默认 true) |
135 | `orderNum` | number | 否 | 排序(默认 0) | 151 | `orderNum` | number | 否 | 排序(默认 0) |
136 152
@@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi... @@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi...
140 { 156 {
141 "categoryCode": "CAT_PREP", 157 "categoryCode": "CAT_PREP",
142 "categoryName": "Prep", 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 "state": true, 164 "state": true,
145 "orderNum": 100 165 "orderNum": 100
146 } 166 }
@@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi... @@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi...
162 { 182 {
163 "categoryCode": "CAT_PREP", 183 "categoryCode": "CAT_PREP",
164 "categoryName": "Prep", 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 "state": true, 190 "state": true,
167 "orderNum": 100 191 "orderNum": 100
168 } 192 }
@@ -179,7 +203,7 @@ Authorization: Bearer eyJhbGciOi... @@ -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,5 +224,5 @@ Authorization: Bearer eyJhbGciOi...
200 推荐前端流程: 224 推荐前端流程:
201 225
202 1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url` 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,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 | `fileName` | string | 服务器保存的文件名 | 58 | `fileName` | string | 服务器保存的文件名 |
59 | `size` | number | 文件大小(字节) | 59 | `size` | number | 文件大小(字节) |
60 60
@@ -96,7 +96,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^ @@ -96,7 +96,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^
96 推荐前端流程: 96 推荐前端流程:
97 97
98 1. 调用本上传接口,拿到返回的 `url` 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,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 ### 1.2 详情 60 ### 1.2 详情
55 61
56 方法:`GET /api/app/label-category/{id}` 62 方法:`GET /api/app/label-category/{id}`
@@ -69,7 +75,11 @@ Swagger 地址: @@ -69,7 +75,11 @@ Swagger 地址:
69 { 75 {
70 "categoryCode": "CAT_PREP", 76 "categoryCode": "CAT_PREP",
71 "categoryName": "Prep", 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 "state": true, 83 "state": true,
74 "orderNum": 1 84 "orderNum": 1
75 } 85 }
@@ -85,7 +95,11 @@ Swagger 地址: @@ -85,7 +95,11 @@ Swagger 地址:
85 { 95 {
86 "categoryCode": "CAT_PREP", 96 "categoryCode": "CAT_PREP",
87 "categoryName": "Prep", 97 "categoryName": "Prep",
88 - "categoryPhotoUrl": null, 98 + "displayText": "Prep",
  99 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  100 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  101 + "availabilityType": "ALL",
  102 + "locationIds": [],
89 "state": true, 103 "state": true,
90 "orderNum": 2 104 "orderNum": 2
91 } 105 }
@@ -560,6 +574,10 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -560,6 +574,10 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
560 入参: 574 入参:
561 - `id`:产品Id(`fl_product.Id`) 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 ### 6.3 新增产品 581 ### 6.3 新增产品
564 582
565 方法:`POST /api/app/product` 583 方法:`POST /api/app/product`
@@ -569,15 +587,30 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -569,15 +587,30 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
569 { 587 {
570 "productCode": "PRD_TEST_001", 588 "productCode": "PRD_TEST_001",
571 "productName": "Chicken", 589 "productName": "Chicken",
572 - "categoryName": "Meat", 590 + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
573 "productImageUrl": "https://example.com/img.png", 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 - `productCode` 不能与未删除的数据重复 612 - `productCode` 不能与未删除的数据重复
  613 +- 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」)
581 614
582 ### 6.4 编辑产品 615 ### 6.4 编辑产品
583 616
@@ -585,7 +618,13 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -585,7 +618,13 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
585 618
586 入参: 619 入参:
587 - Path:`id` 为当前产品Id(`fl_product.Id`) 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 ### 6.5 删除(逻辑删除) 629 ### 6.5 删除(逻辑删除)
591 630
@@ -599,6 +638,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -599,6 +638,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
599 638
600 说明: 639 说明:
601 - 关联表:`fl_location_product` 640 - 关联表:`fl_location_product`
  641 +- 也可在 **§6.3 / §6.4** 通过产品 Body 的 **`locationIds`** 一次性维护本产品在各门店的关联(与 §7 写入同一张表);二者可并存,按需选择调用方式。
602 - 关联按门店进行批量替换: 642 - 关联按门店进行批量替换:
603 - `Create`:在门店下新增未存在的 product 关联 643 - `Create`:在门店下新增未存在的 product 关联
604 - `Update`:替换该门店下全部关联(先删后建) 644 - `Update`:替换该门店下全部关联(先删后建)
@@ -706,7 +746,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -706,7 +746,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
706 |------|------|------| 746 |------|------|------|
707 | `id` | string | `fl_label_category.Id` | 747 | `id` | string | `fl_label_category.Id` |
708 | `categoryName` | string | 分类名称 | 748 | `categoryName` | string | 分类名称 |
709 -| `categoryPhotoUrl` | string \| null | 分类图标/图 | 749 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) |
  750 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) |
710 | `orderNum` | number | 排序 | 751 | `orderNum` | number | 排序 |
711 | `productCategories` | array | 第二级列表(见下表) | 752 | `productCategories` | array | 第二级列表(见下表) |
712 753
@@ -715,8 +756,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -715,8 +756,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
715 | 字段 | 类型 | 说明 | 756 | 字段 | 类型 | 说明 |
716 |------|------|------| 757 |------|------|------|
717 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | 758 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
718 -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | 759 +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 |
719 | `name` | string | 产品分类显示名;空源数据为 **`无`** | 760 | `name` | string | 产品分类显示名;空源数据为 **`无`** |
  761 +| `displayText` | string \| null | 按钮展示文案 |
  762 +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
  763 +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
  764 +| `orderNum` | number | 排序 |
720 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | 765 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
721 | `products` | array | 第三级产品列表(见下表) | 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,13 +816,18 @@ curl -X GET &quot;http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati
771 { 816 {
772 "id": "cat-prep-id", 817 "id": "cat-prep-id",
773 "categoryName": "Prep", 818 "categoryName": "Prep",
774 - "categoryPhotoUrl": "/picture/...", 819 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  820 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
775 "orderNum": 1, 821 "orderNum": 1,
776 "productCategories": [ 822 "productCategories": [
777 { 823 {
778 "categoryId": "pc-meat-id", 824 "categoryId": "pc-meat-id",
779 - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", 825 + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]",
780 "name": "Meat", 826 "name": "Meat",
  827 + "displayText": "Meat",
  828 + "buttonAppearance": "[\"IMAGE\"]",
  829 + "availabilityType": "ALL",
  830 + "orderNum": 10,
781 "itemCount": 1, 831 "itemCount": 1,
782 "products": [ 832 "products": [
783 { 833 {
项目相关文档/美国版App登录接口说明.md
@@ -149,11 +149,294 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... @@ -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 两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。 442 两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。