Commit 395c9e97b0d50753a75631c04266690e61f76e40
合并
Showing
97 changed files
with
5923 additions
and
231 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 当前登录用户简要信息(不含敏感字段) | |
| 5 | +/// </summary> | |
| 6 | +public class CurrentUserBriefDto | |
| 7 | +{ | |
| 8 | + public Guid Id { get; set; } | |
| 9 | + | |
| 10 | + public string UserName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string? Nick { get; set; } | |
| 13 | + | |
| 14 | + public string? Email { get; set; } | |
| 15 | + | |
| 16 | + public string? Icon { get; set; } | |
| 17 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuNodeDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 当前用户可见菜单树节点(与权限分配一致) | |
| 5 | +/// </summary> | |
| 6 | +public class CurrentUserMenuNodeDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string ParentId { get; set; } = "0"; | |
| 11 | + | |
| 12 | + public string MenuName { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string? RouterName { get; set; } | |
| 15 | + | |
| 16 | + public string? Router { get; set; } | |
| 17 | + | |
| 18 | + public string? PermissionCode { get; set; } | |
| 19 | + | |
| 20 | + public int MenuType { get; set; } | |
| 21 | + | |
| 22 | + public int MenuSource { get; set; } | |
| 23 | + | |
| 24 | + public int OrderNum { get; set; } | |
| 25 | + | |
| 26 | + public bool State { get; set; } | |
| 27 | + | |
| 28 | + public string? MenuIcon { get; set; } | |
| 29 | + | |
| 30 | + public string? Component { get; set; } | |
| 31 | + | |
| 32 | + public bool IsLink { get; set; } | |
| 33 | + | |
| 34 | + public bool IsCache { get; set; } | |
| 35 | + | |
| 36 | + public bool IsShow { get; set; } | |
| 37 | + | |
| 38 | + public string? Query { get; set; } | |
| 39 | + | |
| 40 | + public string? Remark { get; set; } | |
| 41 | + | |
| 42 | + public List<CurrentUserMenuNodeDto> Children { get; set; } = new(); | |
| 43 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 当前登录用户的菜单与权限码(用于前端动态路由/按钮权限) | |
| 5 | +/// </summary> | |
| 6 | +public class CurrentUserMenuPermissionsOutputDto | |
| 7 | +{ | |
| 8 | + public CurrentUserBriefDto User { get; set; } = new(); | |
| 9 | + | |
| 10 | + public List<string> RoleCodes { get; set; } = new(); | |
| 11 | + | |
| 12 | + public List<string> PermissionCodes { get; set; } = new(); | |
| 13 | + | |
| 14 | + public List<CurrentUserMenuNodeDto> Menus { get; set; } = new(); | |
| 15 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardCategoryDistributionDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 分类分布项 | |
| 5 | +/// </summary> | |
| 6 | +public class DashboardCategoryDistributionDto | |
| 7 | +{ | |
| 8 | + /// <summary>分类Id</summary> | |
| 9 | + public string CategoryId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>分类名称</summary> | |
| 12 | + public string CategoryName { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>数量</summary> | |
| 15 | + public int Count { get; set; } | |
| 16 | + | |
| 17 | + /// <summary>占比(百分比)</summary> | |
| 18 | + public decimal Ratio { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardDailyTrendPointDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 日趋势点 | |
| 5 | +/// </summary> | |
| 6 | +public class DashboardDailyTrendPointDto | |
| 7 | +{ | |
| 8 | + /// <summary>日期(yyyy-MM-dd)</summary> | |
| 9 | + public string Date { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>值</summary> | |
| 12 | + public int Value { get; set; } | |
| 13 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardMetricCardDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 仪表盘指标卡片 | |
| 5 | +/// </summary> | |
| 6 | +public class DashboardMetricCardDto | |
| 7 | +{ | |
| 8 | + /// <summary>指标唯一标识(如 labelsPrintedToday)</summary> | |
| 9 | + public string Key { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>指标标题</summary> | |
| 12 | + public string Title { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>当前值</summary> | |
| 15 | + public int Value { get; set; } | |
| 16 | + | |
| 17 | + /// <summary>对比周期值</summary> | |
| 18 | + public int PreviousValue { get; set; } | |
| 19 | + | |
| 20 | + /// <summary>增减值(Value - PreviousValue)</summary> | |
| 21 | + public int ChangeValue { get; set; } | |
| 22 | + | |
| 23 | + /// <summary>增减比例(百分比)</summary> | |
| 24 | + public decimal ChangeRate { get; set; } | |
| 25 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardOverviewOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 仪表盘总览输出 | |
| 5 | +/// </summary> | |
| 6 | +public class DashboardOverviewOutputDto | |
| 7 | +{ | |
| 8 | + /// <summary>今日打印标签</summary> | |
| 9 | + public DashboardMetricCardDto LabelsPrintedToday { get; set; } = new(); | |
| 10 | + | |
| 11 | + /// <summary>启用模板数</summary> | |
| 12 | + public DashboardMetricCardDto ActiveTemplates { get; set; } = new(); | |
| 13 | + | |
| 14 | + /// <summary>活跃用户数</summary> | |
| 15 | + public DashboardMetricCardDto ActiveUsers { get; set; } = new(); | |
| 16 | + | |
| 17 | + /// <summary>门店数</summary> | |
| 18 | + public DashboardMetricCardDto Locations { get; set; } = new(); | |
| 19 | + | |
| 20 | + /// <summary>人员数</summary> | |
| 21 | + public DashboardMetricCardDto People { get; set; } = new(); | |
| 22 | + | |
| 23 | + /// <summary>产品数</summary> | |
| 24 | + public DashboardMetricCardDto Products { get; set; } = new(); | |
| 25 | + | |
| 26 | + /// <summary>指标卡片</summary> | |
| 27 | + public List<DashboardMetricCardDto> MetricCards { get; set; } = new(); | |
| 28 | + | |
| 29 | + /// <summary>近7天打印趋势</summary> | |
| 30 | + public List<DashboardDailyTrendPointDto> WeeklyPrintVolume { get; set; } = new(); | |
| 31 | + | |
| 32 | + /// <summary>按分类分布</summary> | |
| 33 | + public List<DashboardCategoryDistributionDto> CategoryDistribution { get; set; } = new(); | |
| 34 | + | |
| 35 | + /// <summary>分类分布总数</summary> | |
| 36 | + public int CategoryDistributionTotal { get; set; } | |
| 37 | + | |
| 38 | + /// <summary>按分类分布(前端直观命名)</summary> | |
| 39 | + public List<DashboardCategoryDistributionDto> ByCategory { get; set; } = new(); | |
| 40 | + | |
| 41 | + /// <summary>按分类分布总数(前端直观命名)</summary> | |
| 42 | + public int ByCategoryTotal { get; set; } | |
| 43 | + | |
| 44 | + /// <summary>最近打印标签(全门店最新若干条,用于 Recent Labels 区块)</summary> | |
| 45 | + public List<DashboardRecentLabelItemDto> RecentLabels { get; set; } = new(); | |
| 46 | + | |
| 47 | + /// <summary>统计时间</summary> | |
| 48 | + public DateTime GeneratedAt { get; set; } | |
| 49 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Dashboard「Recent Labels」单行数据(最近打印记录) | |
| 5 | +/// </summary> | |
| 6 | +public class DashboardRecentLabelItemDto | |
| 7 | +{ | |
| 8 | + /// <summary>打印任务 Id(fl_label_print_task.Id)</summary> | |
| 9 | + public string TaskId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>标签编码(界面 Serial / Label ID,如 1-251201)</summary> | |
| 12 | + public string LabelCode { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>展示名称:优先产品名,否则标签名称</summary> | |
| 15 | + public string DisplayName { get; set; } = "无"; | |
| 16 | + | |
| 17 | + /// <summary>打印人用户 Id(CreatedBy)</summary> | |
| 18 | + public string? PrintedByUserId { get; set; } | |
| 19 | + | |
| 20 | + /// <summary>打印人展示名(User.Name 或 UserName)</summary> | |
| 21 | + public string PrintedByName { get; set; } = "无"; | |
| 22 | + | |
| 23 | + /// <summary>打印时间(PrintedAt 优先,否则 CreationTime)</summary> | |
| 24 | + public DateTime PrintedAt { get; set; } | |
| 25 | + | |
| 26 | + /// <summary>状态:<c>active</c> 或 <c>expired</c>(依据 PrintInputJson 中保质期与当前日期比较)</summary> | |
| 27 | + public string Status { get; set; } = "active"; | |
| 28 | + | |
| 29 | + /// <summary>角标/尺寸短文案(如 2"x2",用于左侧圆标)</summary> | |
| 30 | + public string LabelTypeBadge { get; set; } = "无"; | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 新建组织入参 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupCreateInputVo | |
| 7 | +{ | |
| 8 | + public string GroupName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + /// <summary> | |
| 11 | + /// 指派到的合作伙伴 Id(Assign to Partner) | |
| 12 | + /// </summary> | |
| 13 | + public string PartnerId { get; set; } = string.Empty; | |
| 14 | + | |
| 15 | + public bool State { get; set; } = true; | |
| 16 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 组织(Group)分页查询入参 | |
| 7 | +/// </summary> | |
| 8 | +public class GroupGetListInputVo : PagedAndSortedResultRequestDto | |
| 9 | +{ | |
| 10 | + /// <summary> | |
| 11 | + /// 模糊搜索(GroupName、所属 Partner 的 PartnerName) | |
| 12 | + /// </summary> | |
| 13 | + public string? Keyword { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 按所属合作伙伴筛选(fl_partner.Id) | |
| 17 | + /// </summary> | |
| 18 | + public string? PartnerId { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 启用状态 | |
| 22 | + /// </summary> | |
| 23 | + public bool? State { get; set; } | |
| 24 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 组织列表项 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupGetListOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string GroupName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string PartnerId { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary> | |
| 15 | + /// 所属合作伙伴名称(列表「Parent Partner」列) | |
| 16 | + /// </summary> | |
| 17 | + public string PartnerName { get; set; } = string.Empty; | |
| 18 | + | |
| 19 | + public bool State { get; set; } | |
| 20 | + | |
| 21 | + public DateTime CreationTime { get; set; } | |
| 22 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 组织详情 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupGetOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string GroupName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string PartnerId { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string PartnerName { get; set; } = string.Empty; | |
| 15 | + | |
| 16 | + public bool State { get; set; } | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | + | |
| 20 | + public DateTime? LastModificationTime { get; set; } | |
| 21 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 编辑组织入参 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupUpdateInputVo | |
| 7 | +{ | |
| 8 | + public string GroupName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string PartnerId { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public bool State { get; set; } = true; | |
| 13 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs
| ... | ... | @@ -2,7 +2,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Label; |
| 2 | 2 | |
| 3 | 3 | public class LabelCreateInputVo |
| 4 | 4 | { |
| 5 | - public string LabelCode { get; set; } = string.Empty; | |
| 5 | + public string? LabelCode { get; set; } | |
| 6 | 6 | |
| 7 | 7 | public string LabelName { get; set; } = string.Empty; |
| 8 | 8 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs
| ... | ... | @@ -6,10 +6,33 @@ public class LabelCategoryCreateInputVo |
| 6 | 6 | |
| 7 | 7 | public string CategoryName { get; set; } = string.Empty; |
| 8 | 8 | |
| 9 | + /// <summary> | |
| 10 | + /// 按钮展示文案(为空则默认使用 CategoryName) | |
| 11 | + /// </summary> | |
| 12 | + public string? DisplayText { get; set; } | |
| 13 | + | |
| 14 | + /// <summary> | |
| 15 | + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合) | |
| 16 | + /// </summary> | |
| 9 | 17 | public string? CategoryPhotoUrl { get; set; } |
| 10 | 18 | |
| 11 | 19 | public bool State { get; set; } = true; |
| 12 | 20 | |
| 21 | + /// <summary> | |
| 22 | + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl) | |
| 23 | + /// </summary> | |
| 24 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 门店可用范围:ALL / SPECIFIED | |
| 28 | + /// </summary> | |
| 29 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) | |
| 33 | + /// </summary> | |
| 34 | + public List<string>? LocationIds { get; set; } | |
| 35 | + | |
| 13 | 36 | public int OrderNum { get; set; } |
| 14 | 37 | } |
| 15 | 38 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs
| ... | ... | @@ -8,10 +8,16 @@ public class LabelCategoryGetListOutputDto |
| 8 | 8 | |
| 9 | 9 | public string CategoryName { get; set; } = string.Empty; |
| 10 | 10 | |
| 11 | + public string? DisplayText { get; set; } | |
| 12 | + | |
| 11 | 13 | public string? CategoryPhotoUrl { get; set; } |
| 12 | 14 | |
| 13 | 15 | public bool State { get; set; } |
| 14 | 16 | |
| 17 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 18 | + | |
| 19 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 20 | + | |
| 15 | 21 | public int OrderNum { get; set; } |
| 16 | 22 | |
| 17 | 23 | public long NoOfLabels { get; set; } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs
| ... | ... | @@ -8,10 +8,18 @@ public class LabelCategoryGetOutputDto |
| 8 | 8 | |
| 9 | 9 | public string CategoryName { get; set; } = string.Empty; |
| 10 | 10 | |
| 11 | + public string? DisplayText { get; set; } | |
| 12 | + | |
| 11 | 13 | public string? CategoryPhotoUrl { get; set; } |
| 12 | 14 | |
| 13 | 15 | public bool State { get; set; } |
| 14 | 16 | |
| 17 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 18 | + | |
| 19 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 20 | + | |
| 21 | + public List<string> LocationIds { get; set; } = new(); | |
| 22 | + | |
| 15 | 23 | public int OrderNum { get; set; } |
| 16 | 24 | } |
| 17 | 25 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs
| ... | ... | @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; |
| 3 | 3 | namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate; |
| 4 | 4 | |
| 5 | 5 | /// <summary> |
| 6 | -/// 模板元素(对齐你给的 editor JSON:id/type/x/y/width/height/rotation/border/config) | |
| 6 | +/// 模板元素(对齐 editor JSON:id/type/typeAdd/elementName/x/y/width/height/rotation/border/config 等) | |
| 7 | 7 | /// </summary> |
| 8 | 8 | public class LabelTemplateElementDto |
| 9 | 9 | { | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportCreateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 新增门店 Support 联系方式 | |
| 5 | +/// </summary> | |
| 6 | +public class LocationSupportCreateInputVo | |
| 7 | +{ | |
| 8 | + public string SupportPhone { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string SupportEmail { get; set; } = string.Empty; | |
| 11 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportGetOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 门店 Support 联系方式详情 | |
| 5 | +/// </summary> | |
| 6 | +public class LocationSupportGetOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string SupportPhone { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string SupportEmail { get; set; } = string.Empty; | |
| 13 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportUpdateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 编辑门店 Support 联系方式 | |
| 5 | +/// </summary> | |
| 6 | +public class LocationSupportUpdateInputVo | |
| 7 | +{ | |
| 8 | + public string SupportPhone { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string SupportEmail { get; set; } = string.Empty; | |
| 11 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 新建合作伙伴入参 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerCreateInputVo | |
| 7 | +{ | |
| 8 | + public string PartnerName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string? ContactEmail { get; set; } | |
| 11 | + | |
| 12 | + public string? PhoneNumber { get; set; } | |
| 13 | + | |
| 14 | + public bool State { get; set; } = true; | |
| 15 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 合作伙伴分页查询入参 | |
| 7 | +/// </summary> | |
| 8 | +public class PartnerGetListInputVo : PagedAndSortedResultRequestDto | |
| 9 | +{ | |
| 10 | + /// <summary> | |
| 11 | + /// 模糊搜索(PartnerName / ContactEmail / PhoneNumber) | |
| 12 | + /// </summary> | |
| 13 | + public string? Keyword { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 启用状态(与列表筛选一致) | |
| 17 | + /// </summary> | |
| 18 | + public bool? State { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 合作伙伴列表项 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerGetListOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string PartnerName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string? ContactEmail { get; set; } | |
| 13 | + | |
| 14 | + public string? PhoneNumber { get; set; } | |
| 15 | + | |
| 16 | + public bool State { get; set; } | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 合作伙伴详情 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerGetOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string PartnerName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string? ContactEmail { get; set; } | |
| 13 | + | |
| 14 | + public string? PhoneNumber { get; set; } | |
| 15 | + | |
| 16 | + public bool State { get; set; } | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | + | |
| 20 | + public DateTime? LastModificationTime { get; set; } | |
| 21 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 编辑合作伙伴入参 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerUpdateInputVo | |
| 7 | +{ | |
| 8 | + public string PartnerName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string? ContactEmail { get; set; } | |
| 11 | + | |
| 12 | + public string? PhoneNumber { get; set; } | |
| 13 | + | |
| 14 | + public bool State { get; set; } = true; | |
| 15 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
| 1 | +using System.Collections.Generic; | |
| 2 | + | |
| 1 | 3 | namespace FoodLabeling.Application.Contracts.Dtos.Product; |
| 2 | 4 | |
| 3 | 5 | public class ProductCreateInputVo |
| ... | ... | @@ -11,5 +13,11 @@ public class ProductCreateInputVo |
| 11 | 13 | public string? ProductImageUrl { get; set; } |
| 12 | 14 | |
| 13 | 15 | public bool State { get; set; } = true; |
| 16 | + | |
| 17 | + /// <summary> | |
| 18 | + /// 可选。门店 Id 列表;每个 Id 在 fl_location_product 落一行(同一 fl_product 可对应多门店)。 | |
| 19 | + /// 不传或空列表则不在本接口写入门店关联(仍可用 product-location 接口维护)。 | |
| 20 | + /// </summary> | |
| 21 | + public List<string>? LocationIds { get; set; } | |
| 14 | 22 | } |
| 15 | 23 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs
| ... | ... | @@ -9,10 +9,33 @@ public class ProductCategoryCreateInputVo |
| 9 | 9 | |
| 10 | 10 | public string CategoryName { get; set; } = string.Empty; |
| 11 | 11 | |
| 12 | + /// <summary> | |
| 13 | + /// 按钮展示文案(为空则默认使用 CategoryName) | |
| 14 | + /// </summary> | |
| 15 | + public string? DisplayText { get; set; } | |
| 16 | + | |
| 17 | + /// <summary> | |
| 18 | + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合) | |
| 19 | + /// </summary> | |
| 12 | 20 | public string? CategoryPhotoUrl { get; set; } |
| 13 | 21 | |
| 22 | + /// <summary> | |
| 23 | + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl) | |
| 24 | + /// </summary> | |
| 25 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 26 | + | |
| 14 | 27 | public bool State { get; set; } = true; |
| 15 | 28 | |
| 29 | + /// <summary> | |
| 30 | + /// 门店可用范围:ALL / SPECIFIED | |
| 31 | + /// </summary> | |
| 32 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) | |
| 36 | + /// </summary> | |
| 37 | + public List<string>? LocationIds { get; set; } | |
| 38 | + | |
| 16 | 39 | public int OrderNum { get; set; } = 0; |
| 17 | 40 | } |
| 18 | 41 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs
| ... | ... | @@ -11,10 +11,16 @@ public class ProductCategoryGetListOutputDto |
| 11 | 11 | |
| 12 | 12 | public string CategoryName { get; set; } = string.Empty; |
| 13 | 13 | |
| 14 | + public string? DisplayText { get; set; } | |
| 15 | + | |
| 14 | 16 | public string? CategoryPhotoUrl { get; set; } |
| 15 | 17 | |
| 18 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 19 | + | |
| 16 | 20 | public bool State { get; set; } |
| 17 | 21 | |
| 22 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 23 | + | |
| 18 | 24 | public int OrderNum { get; set; } |
| 19 | 25 | |
| 20 | 26 | public DateTime LastEdited { get; set; } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs
| ... | ... | @@ -11,10 +11,19 @@ public class ProductCategoryGetOutputDto |
| 11 | 11 | |
| 12 | 12 | public string CategoryName { get; set; } = string.Empty; |
| 13 | 13 | |
| 14 | + public string? DisplayText { get; set; } | |
| 15 | + | |
| 16 | + /// <summary>COLOR 色值 / IMAGE 图片 URL / TEXT 可选图</summary> | |
| 14 | 17 | public string? CategoryPhotoUrl { get; set; } |
| 15 | 18 | |
| 19 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 20 | + | |
| 16 | 21 | public bool State { get; set; } |
| 17 | 22 | |
| 23 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 24 | + | |
| 25 | + public List<string> LocationIds { get; set; } = new(); | |
| 26 | + | |
| 18 | 27 | public int OrderNum { get; set; } |
| 19 | 28 | } |
| 20 | 29 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacMenu/RbacMenuGetListOutputDto.cs
| ... | ... | @@ -11,6 +11,10 @@ public class RbacMenuGetListOutputDto |
| 11 | 11 | |
| 12 | 12 | public string MenuName { get; set; } = string.Empty; |
| 13 | 13 | |
| 14 | + public string? RouterName { get; set; } | |
| 15 | + | |
| 16 | + public string? Router { get; set; } | |
| 17 | + | |
| 14 | 18 | public string? PermissionCode { get; set; } |
| 15 | 19 | |
| 16 | 20 | public int MenuType { get; set; } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Label Report 聚合结果(卡片 + 图表 + Top 产品表) | |
| 5 | +/// </summary> | |
| 6 | +public class ReportsLabelReportOutputDto | |
| 7 | +{ | |
| 8 | + public ReportsLabelReportSummaryDto Summary { get; set; } = new(); | |
| 9 | + | |
| 10 | + /// <summary>按标签分类的打印量(柱状图)</summary> | |
| 11 | + public List<ReportsCategoryCountDto> LabelsByCategory { get; set; } = new(); | |
| 12 | + | |
| 13 | + /// <summary>折线图:默认统计区间内最后 7 个自然日(按日汇总)</summary> | |
| 14 | + public List<ReportsDailyCountDto> PrintVolumeTrend { get; set; } = new(); | |
| 15 | + | |
| 16 | + /// <summary>用量最高的产品(含占比)</summary> | |
| 17 | + public List<ReportsTopProductRowDto> MostUsedProducts { get; set; } = new(); | |
| 18 | +} | |
| 19 | + | |
| 20 | +public class ReportsLabelReportSummaryDto | |
| 21 | +{ | |
| 22 | + public int TotalLabelsPrinted { get; set; } | |
| 23 | + | |
| 24 | + public int TotalLabelsPrintedPrevPeriod { get; set; } | |
| 25 | + | |
| 26 | + /// <summary>相对上一同长周期变化率(百分比,如 20.1 表示 +20.1%)</summary> | |
| 27 | + public decimal TotalLabelsPrintedChangeRate { get; set; } | |
| 28 | + | |
| 29 | + public string? MostPrintedCategoryName { get; set; } | |
| 30 | + | |
| 31 | + public int MostPrintedCategoryCount { get; set; } | |
| 32 | + | |
| 33 | + public string? TopProductName { get; set; } | |
| 34 | + | |
| 35 | + public int TopProductCount { get; set; } | |
| 36 | + | |
| 37 | + public decimal AvgDailyPrints { get; set; } | |
| 38 | + | |
| 39 | + public decimal AvgDailyPrintsPrevPeriod { get; set; } | |
| 40 | + | |
| 41 | + public decimal AvgDailyPrintsChangeRate { get; set; } | |
| 42 | +} | |
| 43 | + | |
| 44 | +public class ReportsCategoryCountDto | |
| 45 | +{ | |
| 46 | + public string? CategoryId { get; set; } | |
| 47 | + | |
| 48 | + public string? CategoryName { get; set; } | |
| 49 | + | |
| 50 | + public int Count { get; set; } | |
| 51 | +} | |
| 52 | + | |
| 53 | +public class ReportsDailyCountDto | |
| 54 | +{ | |
| 55 | + public string Date { get; set; } = string.Empty; | |
| 56 | + | |
| 57 | + public int Count { get; set; } | |
| 58 | +} | |
| 59 | + | |
| 60 | +public class ReportsTopProductRowDto | |
| 61 | +{ | |
| 62 | + public string? ProductId { get; set; } | |
| 63 | + | |
| 64 | + public string? ProductName { get; set; } | |
| 65 | + | |
| 66 | + public string? CategoryName { get; set; } | |
| 67 | + | |
| 68 | + public int TotalPrinted { get; set; } | |
| 69 | + | |
| 70 | + public decimal UsagePercent { get; set; } | |
| 71 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Label Report 统计与导出共用筛选 | |
| 5 | +/// </summary> | |
| 6 | +public class ReportsLabelReportQueryInputVo | |
| 7 | +{ | |
| 8 | + public string? PartnerId { get; set; } | |
| 9 | + | |
| 10 | + public string? GroupId { get; set; } | |
| 11 | + | |
| 12 | + public string? LocationId { get; set; } | |
| 13 | + | |
| 14 | + public DateTime? StartDate { get; set; } | |
| 15 | + | |
| 16 | + public DateTime? EndDate { get; set; } | |
| 17 | + | |
| 18 | + public string? Keyword { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// Web Reports — Print Log 分页查询 | |
| 7 | +/// </summary> | |
| 8 | +public class ReportsPrintLogGetListInputVo : PagedAndSortedResultRequestDto | |
| 9 | +{ | |
| 10 | + public string? PartnerId { get; set; } | |
| 11 | + | |
| 12 | + public string? GroupId { get; set; } | |
| 13 | + | |
| 14 | + public string? LocationId { get; set; } | |
| 15 | + | |
| 16 | + public DateTime? StartDate { get; set; } | |
| 17 | + | |
| 18 | + public DateTime? EndDate { get; set; } | |
| 19 | + | |
| 20 | + public string? Keyword { get; set; } | |
| 21 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Print Log 列表行(Reports 管理端) | |
| 5 | +/// </summary> | |
| 6 | +public class ReportsPrintLogListItemDto | |
| 7 | +{ | |
| 8 | + /// <summary>打印任务 Id(fl_label_print_task.Id),重打时使用</summary> | |
| 9 | + public string TaskId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>标签编码(展示为 Label ID)</summary> | |
| 12 | + public string LabelCode { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string ProductName { get; set; } = "无"; | |
| 15 | + | |
| 16 | + /// <summary>分类展示名(优先产品分类,否则标签分类)</summary> | |
| 17 | + public string CategoryName { get; set; } = "无"; | |
| 18 | + | |
| 19 | + /// <summary>模板展示(尺寸 + 模板名)</summary> | |
| 20 | + public string TemplateText { get; set; } = "无"; | |
| 21 | + | |
| 22 | + public DateTime PrintedAt { get; set; } | |
| 23 | + | |
| 24 | + /// <summary>打印人姓名</summary> | |
| 25 | + public string PrintedByName { get; set; } = "无"; | |
| 26 | + | |
| 27 | + /// <summary>门店展示:名称 (编码)</summary> | |
| 28 | + public string LocationText { get; set; } = "无"; | |
| 29 | + | |
| 30 | + /// <summary>门店 Id(重打校验用)</summary> | |
| 31 | + public string? LocationId { get; set; } | |
| 32 | + | |
| 33 | + /// <summary>从 PrintInputJson 尽力解析的保质期文本;无则「无」</summary> | |
| 34 | + public string ExpiryDateText { get; set; } = "无"; | |
| 35 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App 修改密码入参 | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppChangePasswordInputVo | |
| 7 | +{ | |
| 8 | + /// <summary>当前密码</summary> | |
| 9 | + public string CurrentPassword { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>新密码</summary> | |
| 12 | + public string NewPassword { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>确认新密码</summary> | |
| 15 | + public string ConfirmNewPassword { get; set; } = string.Empty; | |
| 16 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App「Location」门店详情(与原型字段对齐) | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppLocationDetailOutputDto | |
| 7 | +{ | |
| 8 | + /// <summary>门店主键(Guid 字符串)</summary> | |
| 9 | + public string LocationId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>门店名称</summary> | |
| 12 | + public string LocationName { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>完整地址(街道、城市、州、邮编拼接;无则为「无」)</summary> | |
| 15 | + public string FullAddress { get; set; } = string.Empty; | |
| 16 | + | |
| 17 | + /// <summary>门店电话(来自 location.Phone;空为「无」)</summary> | |
| 18 | + public string StorePhone { get; set; } = string.Empty; | |
| 19 | + | |
| 20 | + /// <summary>营业时间;当前库无字段,固定返回「无」直至业务落库</summary> | |
| 21 | + public string OperatingHours { get; set; } = string.Empty; | |
| 22 | + | |
| 23 | + /// <summary>店长姓名;优先取绑定本店且角色名/编码含 manager 的用户</summary> | |
| 24 | + public string ManagerName { get; set; } = string.Empty; | |
| 25 | + | |
| 26 | + /// <summary>店长电话;同上用户 User.Phone 格式化;无则为「无」</summary> | |
| 27 | + public string ManagerPhone { get; set; } = string.Empty; | |
| 28 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App「我的资料」展示数据(My Profile) | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppMyProfileOutputDto | |
| 7 | +{ | |
| 8 | + /// <summary>全名(Name,无则 Nick,再无则 UserName)</summary> | |
| 9 | + public string FullName { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>邮箱</summary> | |
| 12 | + public string Email { get; set; } = "无"; | |
| 13 | + | |
| 14 | + /// <summary>电话展示(如 +1 (555) 123-4567);无则「无」</summary> | |
| 15 | + public string Phone { get; set; } = "无"; | |
| 16 | + | |
| 17 | + /// <summary>员工号/登录名(当前使用 <c>User.UserName</c>,可与业务约定为工号)</summary> | |
| 18 | + public string EmployeeId { get; set; } = string.Empty; | |
| 19 | + | |
| 20 | + /// <summary>角色展示名(多角色英文逗号拼接,按角色 OrderNum)</summary> | |
| 21 | + public string RoleDisplay { get; set; } = "无"; | |
| 22 | + | |
| 23 | + /// <summary>主角色编码(第一个角色 RoleCode,供前端样式)</summary> | |
| 24 | + public string? PrimaryRoleCode { get; set; } | |
| 25 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs
| ... | ... | @@ -11,6 +11,9 @@ public class UsAppLabelCategoryTreeNodeDto |
| 11 | 11 | |
| 12 | 12 | public string? CategoryPhotoUrl { get; set; } |
| 13 | 13 | |
| 14 | + /// <summary>按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl)</summary> | |
| 15 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 16 | + | |
| 14 | 17 | public int OrderNum { get; set; } |
| 15 | 18 | |
| 16 | 19 | public List<UsAppProductCategoryNodeDto> ProductCategories { get; set; } = new(); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs
| ... | ... | @@ -49,11 +49,6 @@ public class UsAppLabelPrintInputVo |
| 49 | 49 | public JsonElement? PrintInputJson { get; set; } |
| 50 | 50 | |
| 51 | 51 | /// <summary> |
| 52 | - /// 客户端幂等请求 Id(可选);重复相同值时由服务端决定是否直接返回首次结果(见接口文档)。 | |
| 53 | - /// </summary> | |
| 54 | - public string? ClientRequestId { get; set; } | |
| 55 | - | |
| 56 | - /// <summary> | |
| 57 | 52 | /// 打印机Id(可选,若业务需要追踪) |
| 58 | 53 | /// </summary> |
| 59 | 54 | public string? PrinterId { get; set; } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs
| ... | ... | @@ -14,6 +14,18 @@ public class UsAppProductCategoryNodeDto |
| 14 | 14 | /// <summary>分类显示名;空为「无」</summary> |
| 15 | 15 | public string Name { get; set; } = string.Empty; |
| 16 | 16 | |
| 17 | + /// <summary>按钮展示文案;为空时客户端可回退使用 Name</summary> | |
| 18 | + public string? DisplayText { get; set; } | |
| 19 | + | |
| 20 | + /// <summary>按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl)</summary> | |
| 21 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 22 | + | |
| 23 | + /// <summary>门店可用范围:ALL / SPECIFIED(本树已按当前门店过滤)</summary> | |
| 24 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 25 | + | |
| 26 | + /// <summary>排序号(来自 fl_product_category;未归类为较大值以排在后)</summary> | |
| 27 | + public int OrderNum { get; set; } | |
| 28 | + | |
| 17 | 29 | public int ItemCount { get; set; } |
| 18 | 30 | |
| 19 | 31 | public List<UsAppLabelingProductNodeDto> Products { get; set; } = new(); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.AuthSession; | |
| 2 | +using Volo.Abp.Application.Services; | |
| 3 | + | |
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 5 | + | |
| 6 | +/// <summary> | |
| 7 | +/// 当前登录会话:菜单权限与退出(美国版 Web 管理端) | |
| 8 | +/// </summary> | |
| 9 | +public interface IAuthSessionAppService : IApplicationService | |
| 10 | +{ | |
| 11 | + /// <summary> | |
| 12 | + /// 获取当前登录用户的角色编码、权限码与可见菜单树 | |
| 13 | + /// </summary> | |
| 14 | + /// <remarks> | |
| 15 | + /// 与框架 <c>UserManager.GetInfoAsync</c> 一致;用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。 | |
| 16 | + /// </remarks> | |
| 17 | + /// <returns>用户简要信息、权限码与菜单树</returns> | |
| 18 | + /// <response code="200">成功</response> | |
| 19 | + /// <response code="401">未登录或令牌无效</response> | |
| 20 | + /// <response code="500">服务器错误</response> | |
| 21 | + Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync(); | |
| 22 | + | |
| 23 | + /// <summary> | |
| 24 | + /// 退出登录:清除服务端用户信息缓存(JWT 仍由前端丢弃) | |
| 25 | + /// </summary> | |
| 26 | + /// <remarks> | |
| 27 | + /// 与框架 <c>AccountService.PostLogout</c> 一致;未登录时返回 <c>false</c>。 | |
| 28 | + /// </remarks> | |
| 29 | + /// <returns>是否执行了缓存清理(已登录为 true)</returns> | |
| 30 | + /// <response code="200">成功</response> | |
| 31 | + /// <response code="500">服务器错误</response> | |
| 32 | + Task<bool> LogoutAsync(); | |
| 33 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | +using Volo.Abp.Application.Services; | |
| 3 | + | |
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 5 | + | |
| 6 | +/// <summary> | |
| 7 | +/// Dashboard 统计接口(美国版) | |
| 8 | +/// </summary> | |
| 9 | +public interface IDashboardAppService : IApplicationService | |
| 10 | +{ | |
| 11 | + /// <summary> | |
| 12 | + /// 获取 Dashboard 总览统计(卡片 + 周趋势 + 分类分布 + Recent Labels 最近打印) | |
| 13 | + /// </summary> | |
| 14 | + Task<DashboardOverviewOutputDto> GetOverviewAsync(); | |
| 15 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Group; | |
| 3 | +using Microsoft.AspNetCore.Mvc; | |
| 4 | +using Volo.Abp.Application.Dtos; | |
| 5 | +using Volo.Abp.Application.Services; | |
| 6 | + | |
| 7 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// 组织(Group)管理接口(fl_group) | |
| 11 | +/// </summary> | |
| 12 | +public interface IGroupAppService : IApplicationService | |
| 13 | +{ | |
| 14 | + /// <summary> | |
| 15 | + /// 组织分页列表(与导出使用相同筛选条件) | |
| 16 | + /// </summary> | |
| 17 | + /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> | |
| 18 | + Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input); | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 组织详情 | |
| 22 | + /// </summary> | |
| 23 | + Task<GroupGetOutputDto> GetAsync(string id); | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 新增组织 | |
| 27 | + /// </summary> | |
| 28 | + Task<GroupGetOutputDto> CreateAsync(GroupCreateInputVo input); | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 编辑组织 | |
| 32 | + /// </summary> | |
| 33 | + Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input); | |
| 34 | + | |
| 35 | + /// <summary> | |
| 36 | + /// 删除组织(逻辑删除) | |
| 37 | + /// </summary> | |
| 38 | + Task DeleteAsync(string id); | |
| 39 | + | |
| 40 | + /// <summary> | |
| 41 | + /// 按列表相同筛选条件导出组织为 PDF(上限 5000 条) | |
| 42 | + /// </summary> | |
| 43 | + Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input); | |
| 44 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationSupportAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.LocationSupport; | |
| 2 | +using Volo.Abp.Application.Services; | |
| 3 | + | |
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 5 | + | |
| 6 | +/// <summary> | |
| 7 | +/// 全局 Support 联系方式(全平台共用;Web 可增改查,App 仅可查) | |
| 8 | +/// </summary> | |
| 9 | +public interface ILocationSupportAppService : IApplicationService | |
| 10 | +{ | |
| 11 | + /// <summary> | |
| 12 | + /// 查询全局 Support 联系方式(已登录即可;App / Web 共用) | |
| 13 | + /// </summary> | |
| 14 | + Task<LocationSupportGetOutputDto?> GetSupportAsync(); | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 新增全局 Support 联系方式(系统仅允许一条;Web 管理端) | |
| 18 | + /// </summary> | |
| 19 | + /// <param name="input">联系方式</param> | |
| 20 | + Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input); | |
| 21 | + | |
| 22 | + /// <summary> | |
| 23 | + /// 编辑全局 Support 联系方式(Web 管理端) | |
| 24 | + /// </summary> | |
| 25 | + /// <param name="id">联系方式主键</param> | |
| 26 | + /// <param name="input">联系方式</param> | |
| 27 | + Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input); | |
| 28 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 3 | +using Microsoft.AspNetCore.Mvc; | |
| 4 | +using Volo.Abp.Application.Dtos; | |
| 5 | +using Volo.Abp.Application.Services; | |
| 6 | + | |
| 7 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// 合作伙伴管理接口(fl_partner) | |
| 11 | +/// </summary> | |
| 12 | +public interface IPartnerAppService : IApplicationService | |
| 13 | +{ | |
| 14 | + /// <summary> | |
| 15 | + /// 合作伙伴分页列表(与导出使用相同筛选条件) | |
| 16 | + /// </summary> | |
| 17 | + /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> | |
| 18 | + /// <returns>分页数据</returns> | |
| 19 | + /// <response code="200">成功</response> | |
| 20 | + /// <response code="400">参数错误</response> | |
| 21 | + /// <response code="500">服务器错误</response> | |
| 22 | + Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input); | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 合作伙伴详情 | |
| 26 | + /// </summary> | |
| 27 | + /// <param name="id">主键 Id</param> | |
| 28 | + /// <returns>详情</returns> | |
| 29 | + /// <response code="200">成功</response> | |
| 30 | + /// <response code="400">Id 无效</response> | |
| 31 | + /// <response code="500">服务器错误</response> | |
| 32 | + Task<PartnerGetOutputDto> GetAsync(string id); | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 新增合作伙伴 | |
| 36 | + /// </summary> | |
| 37 | + /// <param name="input">名称、邮箱、电话、启用状态</param> | |
| 38 | + /// <returns>新建后的详情</returns> | |
| 39 | + /// <remarks> | |
| 40 | + /// 示例请求: | |
| 41 | + /// ```json | |
| 42 | + /// { | |
| 43 | + /// "partnerName": "Global Foods Inc.", | |
| 44 | + /// "contactEmail": "admin@globalfoods.com", | |
| 45 | + /// "phoneNumber": "+1 (555) 100-2000", | |
| 46 | + /// "state": true | |
| 47 | + /// } | |
| 48 | + /// ``` | |
| 49 | + /// </remarks> | |
| 50 | + /// <response code="200">成功</response> | |
| 51 | + /// <response code="400">校验失败</response> | |
| 52 | + /// <response code="500">服务器错误</response> | |
| 53 | + Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input); | |
| 54 | + | |
| 55 | + /// <summary> | |
| 56 | + /// 编辑合作伙伴 | |
| 57 | + /// </summary> | |
| 58 | + /// <param name="id">主键 Id</param> | |
| 59 | + /// <param name="input">名称、邮箱、电话、启用状态</param> | |
| 60 | + /// <returns>更新后的详情</returns> | |
| 61 | + /// <response code="200">成功</response> | |
| 62 | + /// <response code="400">校验失败或记录不存在</response> | |
| 63 | + /// <response code="500">服务器错误</response> | |
| 64 | + Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input); | |
| 65 | + | |
| 66 | + /// <summary> | |
| 67 | + /// 删除合作伙伴(逻辑删除) | |
| 68 | + /// </summary> | |
| 69 | + /// <param name="id">主键 Id</param> | |
| 70 | + /// <response code="200">成功</response> | |
| 71 | + /// <response code="400">Id 无效或记录不存在</response> | |
| 72 | + /// <response code="500">服务器错误</response> | |
| 73 | + Task DeleteAsync(string id); | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// 按当前列表筛选条件批量导出合作伙伴为 PDF(不分页,上限 5000 条) | |
| 77 | + /// </summary> | |
| 78 | + /// <param name="input">与列表相同的 Keyword、State;分页字段忽略</param> | |
| 79 | + /// <returns>PDF 文件流</returns> | |
| 80 | + /// <remarks> | |
| 81 | + /// 筛选条件需与 <see cref="GetListAsync"/> 一致,便于统计与导出数据对齐。 | |
| 82 | + /// </remarks> | |
| 83 | + /// <response code="200">成功返回 application/pdf</response> | |
| 84 | + /// <response code="400">参数错误</response> | |
| 85 | + /// <response code="500">服务器错误</response> | |
| 86 | + Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input); | |
| 87 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
| ... | ... | @@ -23,11 +23,18 @@ public interface IProductAppService : IApplicationService |
| 23 | 23 | /// <summary> |
| 24 | 24 | /// 新增产品 |
| 25 | 25 | /// </summary> |
| 26 | + /// <remarks> | |
| 27 | + /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 | |
| 28 | + /// </remarks> | |
| 26 | 29 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); |
| 27 | 30 | |
| 28 | 31 | /// <summary> |
| 29 | 32 | /// 编辑产品 |
| 30 | 33 | /// </summary> |
| 34 | + /// <remarks> | |
| 35 | + /// 当请求体包含 <see cref="ProductCreateInputVo.LocationIds"/> 属性时,按该列表整表替换本产品在各门店的关联; | |
| 36 | + /// 不传该属性则不改门店关联(兼容仅改名称/分类等调用)。 | |
| 37 | + /// </remarks> | |
| 31 | 38 | Task<ProductGetOutputDto> UpdateAsync(string id, ProductUpdateInputVo input); |
| 32 | 39 | |
| 33 | 40 | /// <summary> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | |
| 4 | +using Microsoft.AspNetCore.Mvc; | |
| 5 | +using Volo.Abp.Application.Services; | |
| 6 | + | |
| 7 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// Reports(Print Log / Label Report)管理端接口 | |
| 11 | +/// </summary> | |
| 12 | +public interface IReportsAppService : IApplicationService | |
| 13 | +{ | |
| 14 | + /// <summary> | |
| 15 | + /// Print Log 分页列表;角色 <c>admin</c> 可查全部,否则仅当前用户打印记录。 | |
| 16 | + /// </summary> | |
| 17 | + Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync(ReportsPrintLogGetListInputVo input); | |
| 18 | + | |
| 19 | + /// <summary> | |
| 20 | + /// Print Log 导出 PDF(筛选与列表一致,最多 5000 条) | |
| 21 | + /// </summary> | |
| 22 | + Task<IActionResult> ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input); | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 根据历史任务重打(与 App 入参一致);<c>admin</c> 可重打任意用户任务,否则仅本人任务。 | |
| 26 | + /// </summary> | |
| 27 | + Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input); | |
| 28 | + | |
| 29 | + /// <summary> | |
| 30 | + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);<c>admin</c> 统计全部,否则仅当前用户。 | |
| 31 | + /// </summary> | |
| 32 | + Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input); | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// Label Report 导出 PDF | |
| 36 | + /// </summary> | |
| 37 | + Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input); | |
| 38 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
| ... | ... | @@ -17,4 +17,35 @@ public interface IUsAppAuthAppService : IApplicationService |
| 17 | 17 | /// 获取当前登录账号已绑定的门店(用于切换门店等场景) |
| 18 | 18 | /// </summary> |
| 19 | 19 | Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync(); |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 获取当前登录用户资料(My Profile:姓名、邮箱、电话、员工号、角色) | |
| 23 | + /// </summary> | |
| 24 | + /// <returns>资料 DTO</returns> | |
| 25 | + /// <response code="200">成功</response> | |
| 26 | + /// <response code="400">未登录或用户不存在</response> | |
| 27 | + /// <response code="500">服务器错误</response> | |
| 28 | + Task<UsAppMyProfileOutputDto> GetMyProfileAsync(); | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 当前登录用户修改密码(校验原密码与复杂度规则) | |
| 32 | + /// </summary> | |
| 33 | + /// <param name="input">当前密码、新密码、确认密码</param> | |
| 34 | + /// <remarks> | |
| 35 | + /// 新密码需满足:至少 8 位;含大写与小写字母;至少 1 位数字;至少 1 个非字母数字特殊字符。 | |
| 36 | + /// </remarks> | |
| 37 | + /// <response code="200">成功</response> | |
| 38 | + /// <response code="400">参数或校验失败</response> | |
| 39 | + /// <response code="500">服务器错误</response> | |
| 40 | + Task ChangePasswordAsync(UsAppChangePasswordInputVo input); | |
| 41 | + | |
| 42 | + /// <summary> | |
| 43 | + /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) | |
| 44 | + /// </summary> | |
| 45 | + /// <param name="locationId">门店 Guid 字符串</param> | |
| 46 | + /// <returns>店名、地址、电话、营业时间占位、店长信息</returns> | |
| 47 | + /// <response code="200">成功</response> | |
| 48 | + /// <response code="400">参数非法、未绑定或无权限</response> | |
| 49 | + /// <response code="500">服务器错误</response> | |
| 50 | + Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId); | |
| 20 | 51 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
美国版/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
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/CategoryAppearanceStorageHelper.cs
0 → 100644
| 1 | +using System.Text.Json; | |
| 2 | +using Volo.Abp; | |
| 3 | + | |
| 4 | +namespace FoodLabeling.Application.Helpers; | |
| 5 | + | |
| 6 | +/// <summary> | |
| 7 | +/// 将标签/产品类别的按钮外观与展示字段按「JSON 字符串」落库;兼容历史单行 TEXT/COLOR/IMAGE。 | |
| 8 | +/// </summary> | |
| 9 | +public static class CategoryAppearanceStorageHelper | |
| 10 | +{ | |
| 11 | + /// <summary>未传按钮外观时的默认 JSON(与前端数组语义一致)。</summary> | |
| 12 | + public const string DefaultButtonAppearanceJson = """["TEXT"]"""; | |
| 13 | + | |
| 14 | + /// <summary> | |
| 15 | + /// 规范化 <see cref="FoodLabeling.Application.Services.DbModels.FlLabelCategoryDbEntity.ButtonAppearance"/> / | |
| 16 | + /// 产品类别同名字段:落库为合法 JSON 文本,不做整串 ToUpper(避免破坏 JSON)。 | |
| 17 | + /// </summary> | |
| 18 | + public static string NormalizeButtonAppearanceForStorage(string? raw) | |
| 19 | + { | |
| 20 | + if (string.IsNullOrWhiteSpace(raw)) | |
| 21 | + { | |
| 22 | + return DefaultButtonAppearanceJson; | |
| 23 | + } | |
| 24 | + | |
| 25 | + var t = raw.Trim(); | |
| 26 | + var legacy = t.ToUpperInvariant(); | |
| 27 | + if (legacy is "TEXT" or "COLOR" or "IMAGE") | |
| 28 | + { | |
| 29 | + return JsonSerializer.Serialize(new[] { legacy }); | |
| 30 | + } | |
| 31 | + | |
| 32 | + try | |
| 33 | + { | |
| 34 | + using var _ = JsonDocument.Parse(t); | |
| 35 | + return t; | |
| 36 | + } | |
| 37 | + catch (JsonException) | |
| 38 | + { | |
| 39 | + throw new UserFriendlyException("按钮外观格式不正确,须为合法 JSON(或兼容旧的 TEXT/COLOR/IMAGE)"); | |
| 40 | + } | |
| 41 | + } | |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// 规范化 <see cref="FoodLabeling.Application.Services.DbModels.FlLabelCategoryDbEntity.CategoryPhotoUrl"/> / | |
| 45 | + /// 产品类别同名字段:已是 JSON 则原样落库;否则将整段文本序列化为 JSON 字符串(兼容历史单行色值/URL)。 | |
| 46 | + /// </summary> | |
| 47 | + public static string? NormalizeCategoryPhotoUrlForStorage(string? raw) | |
| 48 | + { | |
| 49 | + if (string.IsNullOrWhiteSpace(raw)) | |
| 50 | + { | |
| 51 | + return null; | |
| 52 | + } | |
| 53 | + | |
| 54 | + var t = raw.Trim(); | |
| 55 | + try | |
| 56 | + { | |
| 57 | + using var _ = JsonDocument.Parse(t); | |
| 58 | + return t; | |
| 59 | + } | |
| 60 | + catch (JsonException) | |
| 61 | + { | |
| 62 | + return JsonSerializer.Serialize(t); | |
| 63 | + } | |
| 64 | + } | |
| 65 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs
0 → 100644
| 1 | +using Volo.Abp.Users; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Helpers; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致) | |
| 7 | +/// </summary> | |
| 8 | +public static class ReportsRoleHelper | |
| 9 | +{ | |
| 10 | + /// <summary> | |
| 11 | + /// 是否为管理员:任一角色码等于 <c>admin</c>(忽略大小写)则视为可查看全部打印数据。 | |
| 12 | + /// </summary> | |
| 13 | + public static bool IsAdminRole(ICurrentUser currentUser) | |
| 14 | + { | |
| 15 | + if (currentUser.Roles is null) | |
| 16 | + { | |
| 17 | + return false; | |
| 18 | + } | |
| 19 | + | |
| 20 | + foreach (var r in currentUser.Roles) | |
| 21 | + { | |
| 22 | + if (!string.IsNullOrWhiteSpace(r) && | |
| 23 | + string.Equals(r.Trim(), "admin", StringComparison.OrdinalIgnoreCase)) | |
| 24 | + { | |
| 25 | + return true; | |
| 26 | + } | |
| 27 | + } | |
| 28 | + | |
| 29 | + return false; | |
| 30 | + } | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.AuthSession; | |
| 2 | +using FoodLabeling.Application.Contracts.IServices; | |
| 3 | +using Microsoft.AspNetCore.Authorization; | |
| 4 | +using Microsoft.AspNetCore.Mvc; | |
| 5 | +using Volo.Abp; | |
| 6 | +using Volo.Abp.Application.Services; | |
| 7 | +using Volo.Abp.Caching; | |
| 8 | +using FoodLabeling.Application.Services.DbModels; | |
| 9 | +using Yi.Framework.Rbac.Domain.Entities; | |
| 10 | +using Yi.Framework.Rbac.Domain.Shared.Caches; | |
| 11 | +using Yi.Framework.Rbac.Domain.Shared.Consts; | |
| 12 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 13 | + | |
| 14 | +namespace FoodLabeling.Application.Services; | |
| 15 | + | |
| 16 | +/// <summary> | |
| 17 | +/// 当前登录会话:菜单权限与退出 | |
| 18 | +/// </summary> | |
| 19 | +[Authorize] | |
| 20 | +public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | |
| 21 | +{ | |
| 22 | + private readonly IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache; | |
| 23 | + private readonly ISqlSugarDbContext _dbContext; | |
| 24 | + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository; | |
| 25 | + | |
| 26 | + public AuthSessionAppService( | |
| 27 | + ISqlSugarDbContext dbContext, | |
| 28 | + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository, | |
| 29 | + IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache) | |
| 30 | + { | |
| 31 | + _dbContext = dbContext; | |
| 32 | + _userRepository = userRepository; | |
| 33 | + _userCache = userCache; | |
| 34 | + } | |
| 35 | + | |
| 36 | + /// <inheritdoc /> | |
| 37 | + public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync() | |
| 38 | + { | |
| 39 | + if (!CurrentUser.Id.HasValue) | |
| 40 | + { | |
| 41 | + throw new UserFriendlyException("用户未登录"); | |
| 42 | + } | |
| 43 | + | |
| 44 | + // 避免走 UserManager.GetInfoAsync -> UserRepository.GetUserAllInfoAsync 的导航加载 | |
| 45 | + // 这里直接按 UserRole/RoleMenu/Menu 表关联查询当前用户可见菜单与权限码 | |
| 46 | + var userId = CurrentUser.Id.Value; | |
| 47 | + var user = await _userRepository.GetByIdAsync(userId); | |
| 48 | + if (user is null || user.IsDeleted) | |
| 49 | + { | |
| 50 | + throw new UserFriendlyException("用户不存在"); | |
| 51 | + } | |
| 52 | + | |
| 53 | + List<MenuDbEntity> menus; | |
| 54 | + if (UserConst.Admin.Equals(user.UserName)) | |
| 55 | + { | |
| 56 | + // MenuAggregateRoot(ParentId 为 Guid) 无法兼容 menu.ParentId=0/字符串:这里统一用 MenuDbEntity | |
| 57 | + menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() | |
| 58 | + .Where(x => x.IsDeleted == false) | |
| 59 | + .ToListAsync(); | |
| 60 | + } | |
| 61 | + else | |
| 62 | + { | |
| 63 | + var roleIds = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>() | |
| 64 | + .Where(x => x.UserId == userId) | |
| 65 | + .Select(x => x.RoleId) | |
| 66 | + .ToListAsync(); | |
| 67 | + | |
| 68 | + var roleIdStrs = roleIds.Select(x => x.ToString()).Distinct().ToList(); | |
| 69 | + if (roleIdStrs.Count == 0) | |
| 70 | + { | |
| 71 | + menus = new List<MenuDbEntity>(); | |
| 72 | + } | |
| 73 | + else | |
| 74 | + { | |
| 75 | + var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>() | |
| 76 | + .Where(x => roleIdStrs.Contains(x.RoleId)) | |
| 77 | + .Select(x => x.MenuId) | |
| 78 | + .Distinct() | |
| 79 | + .ToListAsync(); | |
| 80 | + | |
| 81 | + menus = menuIds.Count == 0 | |
| 82 | + ? new List<MenuDbEntity>() | |
| 83 | + : await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() | |
| 84 | + .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id)) | |
| 85 | + .ToListAsync(); | |
| 86 | + } | |
| 87 | + } | |
| 88 | + | |
| 89 | + var menuNodes = menus | |
| 90 | + .Select(MapToNode) | |
| 91 | + .OrderByDescending(x => x.OrderNum) | |
| 92 | + .ThenBy(x => x.MenuName) | |
| 93 | + .ToList(); | |
| 94 | + | |
| 95 | + // 注意:查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤, | |
| 96 | + // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。 | |
| 97 | + // 这里直接使用 JWT 中的角色码(CurrentUser.Roles)返回,避免触发过滤器。 | |
| 98 | + var roleCodes = CurrentUser.Roles?.ToList() ?? new List<string>(); | |
| 99 | + | |
| 100 | + var permissionCodes = menuNodes | |
| 101 | + .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode)) | |
| 102 | + .Select(x => x.PermissionCode!.Trim()) | |
| 103 | + .Distinct() | |
| 104 | + .OrderBy(x => x) | |
| 105 | + .ToList(); | |
| 106 | + | |
| 107 | + return new CurrentUserMenuPermissionsOutputDto | |
| 108 | + { | |
| 109 | + User = new CurrentUserBriefDto | |
| 110 | + { | |
| 111 | + Id = user.Id, | |
| 112 | + UserName = user.UserName, | |
| 113 | + Nick = user.Nick, | |
| 114 | + Email = user.Email, | |
| 115 | + Icon = user.Icon | |
| 116 | + }, | |
| 117 | + RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(), | |
| 118 | + PermissionCodes = permissionCodes, | |
| 119 | + Menus = BuildMenuTree(menuNodes) | |
| 120 | + }; | |
| 121 | + } | |
| 122 | + | |
| 123 | + /// <inheritdoc /> | |
| 124 | + [HttpPost] | |
| 125 | + public virtual async Task<bool> LogoutAsync() | |
| 126 | + { | |
| 127 | + if (!CurrentUser.Id.HasValue) | |
| 128 | + { | |
| 129 | + return false; | |
| 130 | + } | |
| 131 | + | |
| 132 | + await _userCache.RemoveAsync(new UserInfoCacheKey(CurrentUser.Id.Value)); | |
| 133 | + return true; | |
| 134 | + } | |
| 135 | + | |
| 136 | + private static List<CurrentUserMenuNodeDto> BuildMenuTree(List<CurrentUserMenuNodeDto> flat) | |
| 137 | + { | |
| 138 | + var nodes = flat | |
| 139 | + .GroupBy(x => x.Id) | |
| 140 | + .Select(g => g.First()) | |
| 141 | + .ToList(); | |
| 142 | + var byId = nodes.ToDictionary(n => n.Id, n => n); | |
| 143 | + | |
| 144 | + foreach (var n in nodes) | |
| 145 | + { | |
| 146 | + n.Children = new List<CurrentUserMenuNodeDto>(); | |
| 147 | + } | |
| 148 | + | |
| 149 | + var roots = new List<CurrentUserMenuNodeDto>(); | |
| 150 | + foreach (var n in nodes) | |
| 151 | + { | |
| 152 | + var pid = string.IsNullOrWhiteSpace(n.ParentId) ? "0" : n.ParentId.Trim(); | |
| 153 | + if (pid == "0" || pid == "00000000-0000-0000-0000-000000000000") | |
| 154 | + { | |
| 155 | + roots.Add(n); | |
| 156 | + continue; | |
| 157 | + } | |
| 158 | + | |
| 159 | + if (byId.TryGetValue(pid, out var parent)) | |
| 160 | + { | |
| 161 | + parent.Children.Add(n); | |
| 162 | + } | |
| 163 | + else | |
| 164 | + { | |
| 165 | + roots.Add(n); | |
| 166 | + } | |
| 167 | + } | |
| 168 | + | |
| 169 | + SortMenuTree(roots); | |
| 170 | + return roots; | |
| 171 | + } | |
| 172 | + | |
| 173 | + private static CurrentUserMenuNodeDto MapToNode(MenuDbEntity m) | |
| 174 | + { | |
| 175 | + return new CurrentUserMenuNodeDto | |
| 176 | + { | |
| 177 | + Id = m.Id, | |
| 178 | + ParentId = string.IsNullOrWhiteSpace(m.ParentId) ? "0" : m.ParentId.Trim(), | |
| 179 | + MenuName = m.MenuName ?? string.Empty, | |
| 180 | + RouterName = m.RouterName, | |
| 181 | + Router = m.Router, | |
| 182 | + PermissionCode = m.PermissionCode, | |
| 183 | + MenuType = m.MenuType, | |
| 184 | + MenuSource = m.MenuSource, | |
| 185 | + OrderNum = m.OrderNum, | |
| 186 | + State = m.State, | |
| 187 | + MenuIcon = m.MenuIcon, | |
| 188 | + Component = m.Component, | |
| 189 | + IsLink = m.IsLink, | |
| 190 | + IsCache = m.IsCache, | |
| 191 | + IsShow = m.IsShow, | |
| 192 | + Query = m.Query, | |
| 193 | + Remark = m.Remark | |
| 194 | + }; | |
| 195 | + } | |
| 196 | + | |
| 197 | + private static void SortMenuTree(List<CurrentUserMenuNodeDto> level) | |
| 198 | + { | |
| 199 | + level.Sort((a, b) => | |
| 200 | + { | |
| 201 | + var o = b.OrderNum.CompareTo(a.OrderNum); | |
| 202 | + return o != 0 ? o : string.Compare(a.MenuName, b.MenuName, StringComparison.Ordinal); | |
| 203 | + }); | |
| 204 | + | |
| 205 | + foreach (var n in level) | |
| 206 | + { | |
| 207 | + if (n.Children.Count > 0) | |
| 208 | + { | |
| 209 | + SortMenuTree(n.Children); | |
| 210 | + } | |
| 211 | + } | |
| 212 | + } | |
| 213 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs
0 → 100644
| 1 | +using System.Globalization; | |
| 2 | +using System.Text.Json; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 4 | +using FoodLabeling.Application.Contracts.IServices; | |
| 5 | +using FoodLabeling.Application.Services.DbModels; | |
| 6 | +using FoodLabeling.Domain.Entities; | |
| 7 | +using SqlSugar; | |
| 8 | +using Volo.Abp.Application.Services; | |
| 9 | +using Yi.Framework.Rbac.Domain.Entities; | |
| 10 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 11 | + | |
| 12 | +namespace FoodLabeling.Application.Services; | |
| 13 | + | |
| 14 | +/// <summary> | |
| 15 | +/// Dashboard 统计服务(美国版) | |
| 16 | +/// </summary> | |
| 17 | +public class DashboardAppService : ApplicationService, IDashboardAppService | |
| 18 | +{ | |
| 19 | + private readonly ISqlSugarDbContext _dbContext; | |
| 20 | + private readonly ISqlSugarRepository<LocationAggregateRoot, Guid> _locationRepository; | |
| 21 | + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository; | |
| 22 | + | |
| 23 | + public DashboardAppService( | |
| 24 | + ISqlSugarDbContext dbContext, | |
| 25 | + ISqlSugarRepository<LocationAggregateRoot, Guid> locationRepository, | |
| 26 | + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository) | |
| 27 | + { | |
| 28 | + _dbContext = dbContext; | |
| 29 | + _locationRepository = locationRepository; | |
| 30 | + _userRepository = userRepository; | |
| 31 | + } | |
| 32 | + | |
| 33 | + /// <inheritdoc /> | |
| 34 | + public async Task<DashboardOverviewOutputDto> GetOverviewAsync() | |
| 35 | + { | |
| 36 | + var now = DateTime.Now; | |
| 37 | + var todayStart = now.Date; | |
| 38 | + var tomorrowStart = todayStart.AddDays(1); | |
| 39 | + var yesterdayStart = todayStart.AddDays(-1); | |
| 40 | + var weekStart = todayStart.AddDays(-6); | |
| 41 | + var prevWeekStart = todayStart.AddDays(-13); | |
| 42 | + | |
| 43 | + var printedToday = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 44 | + .CountAsync(x => x.CreationTime >= todayStart && x.CreationTime < tomorrowStart); | |
| 45 | + | |
| 46 | + var printedYesterday = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 47 | + .CountAsync(x => x.CreationTime >= yesterdayStart && x.CreationTime < todayStart); | |
| 48 | + | |
| 49 | + var activeTemplates = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>() | |
| 50 | + .CountAsync(x => !x.IsDeleted && x.State); | |
| 51 | + var activeTemplatesPrevWeek = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>() | |
| 52 | + .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart); | |
| 53 | + | |
| 54 | + var activeUsers = await _userRepository._DbQueryable | |
| 55 | + .CountAsync(x => !x.IsDeleted && x.State); | |
| 56 | + var activeUsersPrevWeek = await _userRepository._DbQueryable | |
| 57 | + .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart); | |
| 58 | + | |
| 59 | + var locations = await _locationRepository._DbQueryable | |
| 60 | + .CountAsync(x => !x.IsDeleted); | |
| 61 | + var locationsPrevWeek = await _locationRepository._DbQueryable | |
| 62 | + .CountAsync(x => !x.IsDeleted && x.CreationTime < weekStart); | |
| 63 | + | |
| 64 | + var people = await _userRepository._DbQueryable | |
| 65 | + .CountAsync(x => !x.IsDeleted); | |
| 66 | + var peoplePrevWeek = await _userRepository._DbQueryable | |
| 67 | + .CountAsync(x => !x.IsDeleted && x.CreationTime < weekStart); | |
| 68 | + | |
| 69 | + var products = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | |
| 70 | + .CountAsync(x => !x.IsDeleted); | |
| 71 | + // fl_product 当前实体未映射 CreationTime,无法按时间切分对比,先回退为同口径总量对比 | |
| 72 | + var productsPrevWeek = products; | |
| 73 | + | |
| 74 | + var weeklyPrintRaw = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 75 | + .Where(x => x.CreationTime >= weekStart && x.CreationTime < tomorrowStart) | |
| 76 | + .Select(x => x.CreationTime) | |
| 77 | + .ToListAsync(); | |
| 78 | + | |
| 79 | + var weeklyDict = weeklyPrintRaw | |
| 80 | + .GroupBy(x => x.Date) | |
| 81 | + .ToDictionary(g => g.Key, g => g.Count()); | |
| 82 | + | |
| 83 | + var weeklyTrend = Enumerable.Range(0, 7) | |
| 84 | + .Select(i => | |
| 85 | + { | |
| 86 | + var d = weekStart.AddDays(i).Date; | |
| 87 | + return new DashboardDailyTrendPointDto | |
| 88 | + { | |
| 89 | + Date = d.ToString("yyyy-MM-dd"), | |
| 90 | + Value = weeklyDict.TryGetValue(d, out var v) ? v : 0 | |
| 91 | + }; | |
| 92 | + }) | |
| 93 | + .ToList(); | |
| 94 | + | |
| 95 | + var categories = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | |
| 96 | + .Where(x => !x.IsDeleted && x.State) | |
| 97 | + .ToListAsync(); | |
| 98 | + | |
| 99 | + var labelCategoryIds = categories.Select(x => x.Id).ToList(); | |
| 100 | + var labelRows = labelCategoryIds.Count == 0 | |
| 101 | + ? new List<string?>() | |
| 102 | + : await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | |
| 103 | + .Where(x => !x.IsDeleted && x.LabelCategoryId != null && labelCategoryIds.Contains(x.LabelCategoryId)) | |
| 104 | + .Select(x => x.LabelCategoryId) | |
| 105 | + .ToListAsync(); | |
| 106 | + | |
| 107 | + var labelCountByCategory = labelRows | |
| 108 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 109 | + .GroupBy(x => x!) | |
| 110 | + .ToDictionary(g => g.Key, g => g.Count()); | |
| 111 | + | |
| 112 | + var categoryDistributionTotal = labelCountByCategory.Values.Sum(); | |
| 113 | + var categoryDistribution = categories | |
| 114 | + .Select(c => | |
| 115 | + { | |
| 116 | + var count = labelCountByCategory.TryGetValue(c.Id, out var v) ? v : 0; | |
| 117 | + var ratio = categoryDistributionTotal == 0 | |
| 118 | + ? 0m | |
| 119 | + : Math.Round(count * 100m / categoryDistributionTotal, 2); | |
| 120 | + return new DashboardCategoryDistributionDto | |
| 121 | + { | |
| 122 | + CategoryId = c.Id, | |
| 123 | + CategoryName = c.CategoryName, | |
| 124 | + Count = count, | |
| 125 | + Ratio = ratio | |
| 126 | + }; | |
| 127 | + }) | |
| 128 | + .Where(x => x.Count > 0) | |
| 129 | + .OrderByDescending(x => x.Count) | |
| 130 | + .ThenBy(x => x.CategoryName) | |
| 131 | + .ToList(); | |
| 132 | + | |
| 133 | + const int recentLabelsTake = 10; | |
| 134 | + var recentRaw = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 135 | + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) | |
| 136 | + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id) | |
| 137 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, tpl) => t.TemplateId == tpl.Id) | |
| 138 | + .OrderBy((t, l, p, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) | |
| 139 | + .Take(recentLabelsTake) | |
| 140 | + .Select((t, l, p, tpl) => new | |
| 141 | + { | |
| 142 | + t.Id, | |
| 143 | + LabelCode = l.LabelCode, | |
| 144 | + LabelName = l.LabelName, | |
| 145 | + ProductName = p.ProductName, | |
| 146 | + tpl.Width, | |
| 147 | + tpl.Height, | |
| 148 | + tpl.Unit, | |
| 149 | + t.PrintInputJson, | |
| 150 | + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 151 | + t.CreatedBy | |
| 152 | + }) | |
| 153 | + .ToListAsync(); | |
| 154 | + | |
| 155 | + var recentUserIds = recentRaw | |
| 156 | + .Select(x => x.CreatedBy) | |
| 157 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 158 | + .Select(x => x!.Trim()) | |
| 159 | + .Distinct() | |
| 160 | + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) | |
| 161 | + .Where(x => x.HasValue) | |
| 162 | + .Select(x => x!.Value) | |
| 163 | + .ToList(); | |
| 164 | + | |
| 165 | + var recentUserMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| 166 | + if (recentUserIds.Count > 0) | |
| 167 | + { | |
| 168 | + var users = await _userRepository._DbQueryable | |
| 169 | + .Where(u => !u.IsDeleted && recentUserIds.Contains(u.Id)) | |
| 170 | + .Select(u => new { u.Id, u.Name, u.UserName }) | |
| 171 | + .ToListAsync(); | |
| 172 | + foreach (var u in users) | |
| 173 | + { | |
| 174 | + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); | |
| 175 | + recentUserMap[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; | |
| 176 | + } | |
| 177 | + } | |
| 178 | + | |
| 179 | + var recentLabels = recentRaw.Select(x => | |
| 180 | + { | |
| 181 | + var displayName = !string.IsNullOrWhiteSpace(x.ProductName) | |
| 182 | + ? x.ProductName.Trim() | |
| 183 | + : (string.IsNullOrWhiteSpace(x.LabelName) ? "无" : x.LabelName.Trim()); | |
| 184 | + var printedAt = x.PrintedAt ?? DateTime.MinValue; | |
| 185 | + var status = ResolveRecentLabelStatus(x.PrintInputJson); | |
| 186 | + return new DashboardRecentLabelItemDto | |
| 187 | + { | |
| 188 | + TaskId = x.Id, | |
| 189 | + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), | |
| 190 | + DisplayName = displayName, | |
| 191 | + PrintedByUserId = x.CreatedBy?.Trim(), | |
| 192 | + PrintedByName = ResolveRecentUserName(recentUserMap, x.CreatedBy), | |
| 193 | + PrintedAt = printedAt, | |
| 194 | + Status = status, | |
| 195 | + LabelTypeBadge = FormatTemplateBadge(x.Width, x.Height, x.Unit) | |
| 196 | + }; | |
| 197 | + }).ToList(); | |
| 198 | + | |
| 199 | + var labelsPrintedTodayCard = BuildMetricCard("labelsPrintedToday", "Labels Printed Today", printedToday, printedYesterday); | |
| 200 | + var activeTemplatesCard = BuildMetricCard("activeTemplates", "Active Templates", activeTemplates, activeTemplatesPrevWeek); | |
| 201 | + var activeUsersCard = BuildMetricCard("activeUsers", "Active Users", activeUsers, activeUsersPrevWeek); | |
| 202 | + var locationsCard = BuildMetricCard("locations", "Locations", locations, locationsPrevWeek); | |
| 203 | + var peopleCard = BuildMetricCard("people", "People", people, peoplePrevWeek); | |
| 204 | + var productsCard = BuildMetricCard("products", "Products", products, productsPrevWeek); | |
| 205 | + | |
| 206 | + var output = new DashboardOverviewOutputDto | |
| 207 | + { | |
| 208 | + LabelsPrintedToday = labelsPrintedTodayCard, | |
| 209 | + ActiveTemplates = activeTemplatesCard, | |
| 210 | + ActiveUsers = activeUsersCard, | |
| 211 | + Locations = locationsCard, | |
| 212 | + People = peopleCard, | |
| 213 | + Products = productsCard, | |
| 214 | + MetricCards = new List<DashboardMetricCardDto> | |
| 215 | + { | |
| 216 | + labelsPrintedTodayCard, | |
| 217 | + activeTemplatesCard, | |
| 218 | + activeUsersCard, | |
| 219 | + locationsCard, | |
| 220 | + peopleCard, | |
| 221 | + productsCard | |
| 222 | + }, | |
| 223 | + WeeklyPrintVolume = weeklyTrend, | |
| 224 | + CategoryDistribution = categoryDistribution, | |
| 225 | + CategoryDistributionTotal = categoryDistributionTotal, | |
| 226 | + ByCategory = categoryDistribution, | |
| 227 | + ByCategoryTotal = categoryDistributionTotal, | |
| 228 | + RecentLabels = recentLabels, | |
| 229 | + GeneratedAt = now | |
| 230 | + }; | |
| 231 | + | |
| 232 | + return output; | |
| 233 | + } | |
| 234 | + | |
| 235 | + private static string ResolveRecentUserName(Dictionary<string, string> map, string? createdBy) | |
| 236 | + { | |
| 237 | + if (string.IsNullOrWhiteSpace(createdBy)) | |
| 238 | + { | |
| 239 | + return "无"; | |
| 240 | + } | |
| 241 | + | |
| 242 | + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; | |
| 243 | + } | |
| 244 | + | |
| 245 | + /// <summary> | |
| 246 | + /// 依据 PrintInputJson 中的保质期字段与「当前日期」比较得到 active/expired。 | |
| 247 | + /// </summary> | |
| 248 | + private static string ResolveRecentLabelStatus(string? printInputJson) | |
| 249 | + { | |
| 250 | + if (!TryParseExpiryDate(printInputJson, out var expiryDate)) | |
| 251 | + { | |
| 252 | + return "active"; | |
| 253 | + } | |
| 254 | + | |
| 255 | + var today = DateTime.Now.Date; | |
| 256 | + return expiryDate.Date < today ? "expired" : "active"; | |
| 257 | + } | |
| 258 | + | |
| 259 | + private static bool TryParseExpiryDate(string? printInputJson, out DateTime expiryDate) | |
| 260 | + { | |
| 261 | + expiryDate = default; | |
| 262 | + if (string.IsNullOrWhiteSpace(printInputJson)) | |
| 263 | + { | |
| 264 | + return false; | |
| 265 | + } | |
| 266 | + | |
| 267 | + try | |
| 268 | + { | |
| 269 | + using var doc = JsonDocument.Parse(printInputJson); | |
| 270 | + if (doc.RootElement.ValueKind != JsonValueKind.Object) | |
| 271 | + { | |
| 272 | + return false; | |
| 273 | + } | |
| 274 | + | |
| 275 | + foreach (var prop in doc.RootElement.EnumerateObject()) | |
| 276 | + { | |
| 277 | + var key = prop.Name.Trim(); | |
| 278 | + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && | |
| 279 | + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && | |
| 280 | + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) | |
| 281 | + { | |
| 282 | + continue; | |
| 283 | + } | |
| 284 | + | |
| 285 | + var v = prop.Value; | |
| 286 | + if (v.ValueKind == JsonValueKind.String) | |
| 287 | + { | |
| 288 | + var s = v.GetString(); | |
| 289 | + if (!string.IsNullOrWhiteSpace(s) && | |
| 290 | + DateTime.TryParse(s, CultureInfo.InvariantCulture, | |
| 291 | + DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces, out var dt)) | |
| 292 | + { | |
| 293 | + expiryDate = dt; | |
| 294 | + return true; | |
| 295 | + } | |
| 296 | + } | |
| 297 | + else if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var unix)) | |
| 298 | + { | |
| 299 | + expiryDate = DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime; | |
| 300 | + return true; | |
| 301 | + } | |
| 302 | + } | |
| 303 | + } | |
| 304 | + catch | |
| 305 | + { | |
| 306 | + return false; | |
| 307 | + } | |
| 308 | + | |
| 309 | + return false; | |
| 310 | + } | |
| 311 | + | |
| 312 | + private static string FormatTemplateBadge(decimal w, decimal h, string? unit) | |
| 313 | + { | |
| 314 | + var u = (unit ?? "inch").Trim().ToLowerInvariant(); | |
| 315 | + var ws = w.ToString(CultureInfo.InvariantCulture); | |
| 316 | + var hs = h.ToString(CultureInfo.InvariantCulture); | |
| 317 | + return u is "inch" or "in" | |
| 318 | + ? $"{ws}\"x{hs}\"" | |
| 319 | + : $"{ws}x{hs}{u}"; | |
| 320 | + } | |
| 321 | + | |
| 322 | + private static DashboardMetricCardDto BuildMetricCard(string key, string title, int value, int previousValue) | |
| 323 | + { | |
| 324 | + var changeValue = value - previousValue; | |
| 325 | + var changeRate = previousValue <= 0 | |
| 326 | + ? (value > 0 ? 100m : 0m) | |
| 327 | + : Math.Round(changeValue * 100m / previousValue, 2); | |
| 328 | + | |
| 329 | + return new DashboardMetricCardDto | |
| 330 | + { | |
| 331 | + Key = key, | |
| 332 | + Title = title, | |
| 333 | + Value = value, | |
| 334 | + PreviousValue = previousValue, | |
| 335 | + ChangeValue = changeValue, | |
| 336 | + ChangeRate = changeRate | |
| 337 | + }; | |
| 338 | + } | |
| 339 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 组织/分组(Account Management / Group,表 fl_group) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_group")] | |
| 9 | +public class FlGroupDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public bool IsDeleted { get; set; } | |
| 15 | + | |
| 16 | + public DateTime CreationTime { get; set; } | |
| 17 | + | |
| 18 | + public string? CreatorId { get; set; } | |
| 19 | + | |
| 20 | + public string? LastModifierId { get; set; } | |
| 21 | + | |
| 22 | + public DateTime? LastModificationTime { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 组织名称(Group Name) | |
| 26 | + /// </summary> | |
| 27 | + public string GroupName { get; set; } = string.Empty; | |
| 28 | + | |
| 29 | + /// <summary> | |
| 30 | + /// 所属合作伙伴 Id(fl_partner.Id) | |
| 31 | + /// </summary> | |
| 32 | + public string PartnerId { get; set; } = string.Empty; | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 是否启用(对应 UI Active) | |
| 36 | + /// </summary> | |
| 37 | + public bool State { get; set; } | |
| 38 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs
| ... | ... | @@ -24,10 +24,28 @@ public class FlLabelCategoryDbEntity |
| 24 | 24 | |
| 25 | 25 | public string CategoryName { get; set; } = string.Empty; |
| 26 | 26 | |
| 27 | + /// <summary> | |
| 28 | + /// 按钮展示文案(为空则默认使用 CategoryName) | |
| 29 | + /// </summary> | |
| 30 | + public string? DisplayText { get; set; } | |
| 31 | + | |
| 32 | + /// <summary> | |
| 33 | + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值(如 #409EFF);IMAGE 存图片 URL(与 ButtonAppearance 配合) | |
| 34 | + /// </summary> | |
| 27 | 35 | public string? CategoryPhotoUrl { get; set; } |
| 28 | 36 | |
| 29 | 37 | public int OrderNum { get; set; } |
| 30 | 38 | |
| 31 | 39 | public bool State { get; set; } |
| 40 | + | |
| 41 | + /// <summary> | |
| 42 | + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl) | |
| 43 | + /// </summary> | |
| 44 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 45 | + | |
| 46 | + /// <summary> | |
| 47 | + /// 门店可用范围:ALL / SPECIFIED | |
| 48 | + /// </summary> | |
| 49 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 32 | 50 | } |
| 33 | 51 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryLocationDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 标签分类可用门店关联(对应表:fl_label_category_location) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_label_category_location")] | |
| 9 | +public class FlLabelCategoryLocationDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string CategoryId { get; set; } = string.Empty; | |
| 15 | + | |
| 16 | + public string LocationId { get; set; } = string.Empty; | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | + | |
| 20 | + public string? CreatorId { get; set; } | |
| 21 | +} | |
| 22 | + | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs
| ... | ... | @@ -20,7 +20,7 @@ public class FlLabelDbEntity |
| 20 | 20 | |
| 21 | 21 | public string ConcurrencyStamp { get; set; } = string.Empty; |
| 22 | 22 | |
| 23 | - public string LabelCode { get; set; } = string.Empty; | |
| 23 | + public string? LabelCode { get; set; } | |
| 24 | 24 | |
| 25 | 25 | public string LabelName { get; set; } = string.Empty; |
| 26 | 26 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationSupportDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 门店 Support 联系方式(每个门店仅一条,对 App Support 页展示) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_location_support")] | |
| 9 | +public class FlLocationSupportDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public bool IsDeleted { get; set; } | |
| 15 | + | |
| 16 | + public DateTime CreationTime { get; set; } | |
| 17 | + | |
| 18 | + public string? CreatorId { get; set; } | |
| 19 | + | |
| 20 | + public string? LastModifierId { get; set; } | |
| 21 | + | |
| 22 | + public DateTime? LastModificationTime { get; set; } | |
| 23 | + | |
| 24 | + public string SupportPhone { get; set; } = string.Empty; | |
| 25 | + | |
| 26 | + public string SupportEmail { get; set; } = string.Empty; | |
| 27 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 合作伙伴主数据(Account Management / Partner,表 fl_partner) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_partner")] | |
| 9 | +public class FlPartnerDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public bool IsDeleted { get; set; } | |
| 15 | + | |
| 16 | + public DateTime CreationTime { get; set; } | |
| 17 | + | |
| 18 | + public string? CreatorId { get; set; } | |
| 19 | + | |
| 20 | + public string? LastModifierId { get; set; } | |
| 21 | + | |
| 22 | + public DateTime? LastModificationTime { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 合作伙伴名称(公司名) | |
| 26 | + /// </summary> | |
| 27 | + public string PartnerName { get; set; } = string.Empty; | |
| 28 | + | |
| 29 | + /// <summary> | |
| 30 | + /// 联系邮箱 | |
| 31 | + /// </summary> | |
| 32 | + public string? ContactEmail { get; set; } | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 电话 | |
| 36 | + /// </summary> | |
| 37 | + public string? PhoneNumber { get; set; } | |
| 38 | + | |
| 39 | + /// <summary> | |
| 40 | + /// 是否启用(对应 UI Active) | |
| 41 | + /// </summary> | |
| 42 | + public bool State { get; set; } | |
| 43 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs
| ... | ... | @@ -24,10 +24,28 @@ public class FlProductCategoryDbEntity |
| 24 | 24 | |
| 25 | 25 | public string CategoryName { get; set; } = string.Empty; |
| 26 | 26 | |
| 27 | + /// <summary> | |
| 28 | + /// 按钮展示文案(为空则默认使用 CategoryName) | |
| 29 | + /// </summary> | |
| 30 | + public string? DisplayText { get; set; } | |
| 31 | + | |
| 32 | + /// <summary> | |
| 33 | + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值;IMAGE 存图片 URL(与 ButtonAppearance 配合) | |
| 34 | + /// </summary> | |
| 27 | 35 | public string? CategoryPhotoUrl { get; set; } |
| 28 | 36 | |
| 37 | + /// <summary> | |
| 38 | + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl) | |
| 39 | + /// </summary> | |
| 40 | + public string ButtonAppearance { get; set; } = "TEXT"; | |
| 41 | + | |
| 29 | 42 | public bool State { get; set; } |
| 30 | 43 | |
| 44 | + /// <summary> | |
| 45 | + /// 门店可用范围:ALL / SPECIFIED | |
| 46 | + /// </summary> | |
| 47 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 48 | + | |
| 31 | 49 | public int OrderNum { get; set; } |
| 32 | 50 | } |
| 33 | 51 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryLocationDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 产品类别可用门店关联(对应表:fl_product_category_location) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_product_category_location")] | |
| 9 | +public class FlProductCategoryLocationDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string CategoryId { get; set; } = string.Empty; | |
| 15 | + | |
| 16 | + public string LocationId { get; set; } = string.Empty; | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | + | |
| 20 | + public string? CreatorId { get; set; } | |
| 21 | +} | |
| 22 | + | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Helpers; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.Group; | |
| 4 | +using FoodLabeling.Application.Contracts.IServices; | |
| 5 | +using FoodLabeling.Application.Services.DbModels; | |
| 6 | +using Microsoft.AspNetCore.Mvc; | |
| 7 | +using QuestPDF.Fluent; | |
| 8 | +using QuestPDF.Helpers; | |
| 9 | +using QuestPDF.Infrastructure; | |
| 10 | +using SqlSugar; | |
| 11 | +using Volo.Abp; | |
| 12 | +using Volo.Abp.Application.Services; | |
| 13 | +using Volo.Abp.Guids; | |
| 14 | +using Volo.Abp.Uow; | |
| 15 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 16 | + | |
| 17 | +namespace FoodLabeling.Application.Services; | |
| 18 | + | |
| 19 | +/// <summary> | |
| 20 | +/// 组织(Group)管理(fl_group) | |
| 21 | +/// </summary> | |
| 22 | +public class GroupAppService : ApplicationService, IGroupAppService | |
| 23 | +{ | |
| 24 | + private const int ExportPdfMaxRows = 5000; | |
| 25 | + | |
| 26 | + private readonly ISqlSugarDbContext _dbContext; | |
| 27 | + private readonly IGuidGenerator _guidGenerator; | |
| 28 | + | |
| 29 | + public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | |
| 30 | + { | |
| 31 | + _dbContext = dbContext; | |
| 32 | + _guidGenerator = guidGenerator; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /// <inheritdoc /> | |
| 36 | + public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input) | |
| 37 | + { | |
| 38 | + RefAsync<int> total = 0; | |
| 39 | + var query = BuildGroupJoinedQuery(input); | |
| 40 | + var projected = query.Select((g, p) => new GroupGetListOutputDto | |
| 41 | + { | |
| 42 | + Id = g.Id, | |
| 43 | + GroupName = g.GroupName, | |
| 44 | + PartnerId = g.PartnerId, | |
| 45 | + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), | |
| 46 | + State = g.State, | |
| 47 | + CreationTime = g.CreationTime | |
| 48 | + }); | |
| 49 | + | |
| 50 | + var items = await projected.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | |
| 51 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | |
| 52 | + } | |
| 53 | + | |
| 54 | + /// <inheritdoc /> | |
| 55 | + public async Task<GroupGetOutputDto> GetAsync(string id) | |
| 56 | + { | |
| 57 | + var groupId = id?.Trim(); | |
| 58 | + if (string.IsNullOrWhiteSpace(groupId)) | |
| 59 | + { | |
| 60 | + throw new UserFriendlyException("组织Id不能为空"); | |
| 61 | + } | |
| 62 | + | |
| 63 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 64 | + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | |
| 65 | + if (entity is null) | |
| 66 | + { | |
| 67 | + throw new UserFriendlyException("组织不存在"); | |
| 68 | + } | |
| 69 | + | |
| 70 | + var partnerName = await ResolvePartnerNameAsync(entity.PartnerId); | |
| 71 | + return MapDetail(entity, partnerName); | |
| 72 | + } | |
| 73 | + | |
| 74 | + /// <inheritdoc /> | |
| 75 | + [UnitOfWork] | |
| 76 | + public async Task<GroupGetOutputDto> CreateAsync(GroupCreateInputVo input) | |
| 77 | + { | |
| 78 | + var name = input.GroupName?.Trim(); | |
| 79 | + if (string.IsNullOrWhiteSpace(name)) | |
| 80 | + { | |
| 81 | + throw new UserFriendlyException("组织名称不能为空"); | |
| 82 | + } | |
| 83 | + | |
| 84 | + var partnerId = input.PartnerId?.Trim(); | |
| 85 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 86 | + { | |
| 87 | + throw new UserFriendlyException("请选择所属合作伙伴"); | |
| 88 | + } | |
| 89 | + | |
| 90 | + await EnsurePartnerExistsAsync(partnerId); | |
| 91 | + | |
| 92 | + var now = Clock.Now; | |
| 93 | + var entity = new FlGroupDbEntity | |
| 94 | + { | |
| 95 | + Id = _guidGenerator.Create().ToString(), | |
| 96 | + IsDeleted = false, | |
| 97 | + GroupName = name, | |
| 98 | + PartnerId = partnerId, | |
| 99 | + State = input.State, | |
| 100 | + CreationTime = now, | |
| 101 | + CreatorId = CurrentUser?.Id?.ToString(), | |
| 102 | + LastModificationTime = now, | |
| 103 | + LastModifierId = CurrentUser?.Id?.ToString() | |
| 104 | + }; | |
| 105 | + | |
| 106 | + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | |
| 107 | + return await GetAsync(entity.Id); | |
| 108 | + } | |
| 109 | + | |
| 110 | + /// <inheritdoc /> | |
| 111 | + [UnitOfWork] | |
| 112 | + public async Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input) | |
| 113 | + { | |
| 114 | + var groupId = id?.Trim(); | |
| 115 | + if (string.IsNullOrWhiteSpace(groupId)) | |
| 116 | + { | |
| 117 | + throw new UserFriendlyException("组织Id不能为空"); | |
| 118 | + } | |
| 119 | + | |
| 120 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 121 | + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | |
| 122 | + if (entity is null) | |
| 123 | + { | |
| 124 | + throw new UserFriendlyException("组织不存在"); | |
| 125 | + } | |
| 126 | + | |
| 127 | + var name = input.GroupName?.Trim(); | |
| 128 | + if (string.IsNullOrWhiteSpace(name)) | |
| 129 | + { | |
| 130 | + throw new UserFriendlyException("组织名称不能为空"); | |
| 131 | + } | |
| 132 | + | |
| 133 | + var partnerId = input.PartnerId?.Trim(); | |
| 134 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 135 | + { | |
| 136 | + throw new UserFriendlyException("请选择所属合作伙伴"); | |
| 137 | + } | |
| 138 | + | |
| 139 | + await EnsurePartnerExistsAsync(partnerId); | |
| 140 | + | |
| 141 | + entity.GroupName = name; | |
| 142 | + entity.PartnerId = partnerId; | |
| 143 | + entity.State = input.State; | |
| 144 | + entity.LastModificationTime = Clock.Now; | |
| 145 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 146 | + | |
| 147 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 148 | + return await GetAsync(groupId); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /// <inheritdoc /> | |
| 152 | + [UnitOfWork] | |
| 153 | + public async Task DeleteAsync(string id) | |
| 154 | + { | |
| 155 | + var groupId = id?.Trim(); | |
| 156 | + if (string.IsNullOrWhiteSpace(groupId)) | |
| 157 | + { | |
| 158 | + throw new UserFriendlyException("组织Id不能为空"); | |
| 159 | + } | |
| 160 | + | |
| 161 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 162 | + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | |
| 163 | + if (entity is null) | |
| 164 | + { | |
| 165 | + throw new UserFriendlyException("组织不存在"); | |
| 166 | + } | |
| 167 | + | |
| 168 | + entity.IsDeleted = true; | |
| 169 | + entity.LastModificationTime = Clock.Now; | |
| 170 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 171 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 172 | + } | |
| 173 | + | |
| 174 | + /// <inheritdoc /> | |
| 175 | + public async Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input) | |
| 176 | + { | |
| 177 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 178 | + | |
| 179 | + var exportBase = BuildGroupJoinedQuery(input); | |
| 180 | + var count = await exportBase.CountAsync(); | |
| 181 | + if (count > ExportPdfMaxRows) | |
| 182 | + { | |
| 183 | + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | |
| 184 | + } | |
| 185 | + | |
| 186 | + var rows = await BuildGroupJoinedQuery(input) | |
| 187 | + .Select((g, p) => new GroupGetListOutputDto | |
| 188 | + { | |
| 189 | + Id = g.Id, | |
| 190 | + GroupName = g.GroupName, | |
| 191 | + PartnerId = g.PartnerId, | |
| 192 | + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), | |
| 193 | + State = g.State, | |
| 194 | + CreationTime = g.CreationTime | |
| 195 | + }) | |
| 196 | + .Take(ExportPdfMaxRows) | |
| 197 | + .ToListAsync(); | |
| 198 | + | |
| 199 | + var fileName = $"groups_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 200 | + var document = Document.Create(container => | |
| 201 | + { | |
| 202 | + container.Page(page => | |
| 203 | + { | |
| 204 | + page.Margin(28); | |
| 205 | + page.DefaultTextStyle(x => x.FontSize(10)); | |
| 206 | + page.Header().Text("Groups").SemiBold().FontSize(18); | |
| 207 | + page.Content().PaddingTop(12).Table(table => | |
| 208 | + { | |
| 209 | + table.ColumnsDefinition(c => | |
| 210 | + { | |
| 211 | + c.RelativeColumn(2.2f); | |
| 212 | + c.RelativeColumn(2.4f); | |
| 213 | + c.RelativeColumn(1f); | |
| 214 | + c.RelativeColumn(1.6f); | |
| 215 | + }); | |
| 216 | + | |
| 217 | + static IContainer CellHeader(IContainer c) => | |
| 218 | + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold()); | |
| 219 | + | |
| 220 | + table.Cell().Element(CellHeader).Text("Group Name"); | |
| 221 | + table.Cell().Element(CellHeader).Text("Parent Partner"); | |
| 222 | + table.Cell().Element(CellHeader).Text("Status"); | |
| 223 | + table.Cell().Element(CellHeader).Text("Created"); | |
| 224 | + | |
| 225 | + foreach (var e in rows) | |
| 226 | + { | |
| 227 | + var status = e.State ? "active" : "inactive"; | |
| 228 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 229 | + .Text(e.GroupName ?? string.Empty); | |
| 230 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 231 | + .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "无" : e.PartnerName); | |
| 232 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); | |
| 233 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 234 | + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); | |
| 235 | + } | |
| 236 | + }); | |
| 237 | + }); | |
| 238 | + }); | |
| 239 | + | |
| 240 | + var stream = new MemoryStream(); | |
| 241 | + document.GeneratePdf(stream); | |
| 242 | + stream.Position = 0; | |
| 243 | + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; | |
| 244 | + } | |
| 245 | + | |
| 246 | + private ISugarQueryable<FlGroupDbEntity, FlPartnerDbEntity> BuildGroupJoinedQuery(GroupGetListInputVo input) | |
| 247 | + { | |
| 248 | + var keyword = input.Keyword?.Trim(); | |
| 249 | + var partnerId = input.PartnerId?.Trim(); | |
| 250 | + | |
| 251 | + var query = _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 252 | + .LeftJoin<FlPartnerDbEntity>((g, p) => g.PartnerId == p.Id && !p.IsDeleted) | |
| 253 | + .Where((g, p) => !g.IsDeleted) | |
| 254 | + .WhereIF(input.State != null, (g, p) => g.State == input.State) | |
| 255 | + .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId) | |
| 256 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), | |
| 257 | + (g, p) => g.GroupName.Contains(keyword!) || | |
| 258 | + (p.PartnerName != null && p.PartnerName.Contains(keyword!))); | |
| 259 | + | |
| 260 | + if (!string.IsNullOrWhiteSpace(input.Sorting)) | |
| 261 | + { | |
| 262 | + var sorting = input.Sorting.Trim(); | |
| 263 | + if (sorting.Equals("GroupName desc", StringComparison.OrdinalIgnoreCase)) | |
| 264 | + { | |
| 265 | + query = query.OrderByDescending((g, p) => g.GroupName); | |
| 266 | + } | |
| 267 | + else if (sorting.Equals("GroupName asc", StringComparison.OrdinalIgnoreCase)) | |
| 268 | + { | |
| 269 | + query = query.OrderBy((g, p) => g.GroupName); | |
| 270 | + } | |
| 271 | + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) | |
| 272 | + { | |
| 273 | + query = query.OrderByDescending((g, p) => g.CreationTime); | |
| 274 | + } | |
| 275 | + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) | |
| 276 | + { | |
| 277 | + query = query.OrderBy((g, p) => g.CreationTime); | |
| 278 | + } | |
| 279 | + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase)) | |
| 280 | + { | |
| 281 | + query = query.OrderByDescending((g, p) => g.State); | |
| 282 | + } | |
| 283 | + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase)) | |
| 284 | + { | |
| 285 | + query = query.OrderBy((g, p) => g.State); | |
| 286 | + } | |
| 287 | + else if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase)) | |
| 288 | + { | |
| 289 | + query = query.OrderByDescending((g, p) => p.PartnerName); | |
| 290 | + } | |
| 291 | + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase)) | |
| 292 | + { | |
| 293 | + query = query.OrderBy((g, p) => p.PartnerName); | |
| 294 | + } | |
| 295 | + else | |
| 296 | + { | |
| 297 | + query = query.OrderByDescending((g, p) => g.CreationTime); | |
| 298 | + } | |
| 299 | + } | |
| 300 | + else | |
| 301 | + { | |
| 302 | + query = query.OrderByDescending((g, p) => g.CreationTime); | |
| 303 | + } | |
| 304 | + | |
| 305 | + return query; | |
| 306 | + } | |
| 307 | + | |
| 308 | + private async Task EnsurePartnerExistsAsync(string partnerId) | |
| 309 | + { | |
| 310 | + var ok = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 311 | + .AnyAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 312 | + if (!ok) | |
| 313 | + { | |
| 314 | + throw new UserFriendlyException("所选合作伙伴不存在或已删除"); | |
| 315 | + } | |
| 316 | + } | |
| 317 | + | |
| 318 | + private async Task<string> ResolvePartnerNameAsync(string partnerId) | |
| 319 | + { | |
| 320 | + var p = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 321 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 322 | + if (p is null || string.IsNullOrWhiteSpace(p.PartnerName)) | |
| 323 | + { | |
| 324 | + return "无"; | |
| 325 | + } | |
| 326 | + | |
| 327 | + return p.PartnerName.Trim(); | |
| 328 | + } | |
| 329 | + | |
| 330 | + private static GroupGetOutputDto MapDetail(FlGroupDbEntity x, string partnerName) => new() | |
| 331 | + { | |
| 332 | + Id = x.Id, | |
| 333 | + GroupName = x.GroupName, | |
| 334 | + PartnerId = x.PartnerId, | |
| 335 | + PartnerName = partnerName, | |
| 336 | + State = x.State, | |
| 337 | + CreationTime = x.CreationTime, | |
| 338 | + LastModificationTime = x.LastModificationTime | |
| 339 | + }; | |
| 340 | + | |
| 341 | + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, | |
| 342 | + List<T> items) | |
| 343 | + { | |
| 344 | + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | |
| 345 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); | |
| 346 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | |
| 347 | + return new PagedResultWithPageDto<T> | |
| 348 | + { | |
| 349 | + PageIndex = pageIndex, | |
| 350 | + PageSize = pageSize, | |
| 351 | + TotalCount = total, | |
| 352 | + TotalPages = totalPages, | |
| 353 | + Items = items | |
| 354 | + }; | |
| 355 | + } | |
| 356 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
| ... | ... | @@ -261,7 +261,7 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 261 | 261 | |
| 262 | 262 | return new LabelGetOutputDto |
| 263 | 263 | { |
| 264 | - Id = label.LabelCode, | |
| 264 | + Id = label.LabelCode ?? string.Empty, | |
| 265 | 265 | LabelName = label.LabelName, |
| 266 | 266 | LocationId = label.LocationId ?? string.Empty, |
| 267 | 267 | LocationName = location?.LocationName ?? location?.LocationCode ?? "无", | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
| ... | ... | @@ -30,12 +30,34 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 30 | 30 | var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 31 | 31 | .Where(x => !x.IsDeleted) |
| 32 | 32 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 33 | - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) | |
| 33 | + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) || | |
| 34 | + (x.DisplayText != null && x.DisplayText.Contains(keyword!))) | |
| 34 | 35 | .WhereIF(input.State != null, x => x.State == input.State); |
| 35 | 36 | |
| 37 | + // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 | |
| 36 | 38 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 37 | 39 | { |
| 38 | - query = query.OrderBy(input.Sorting); | |
| 40 | + var sorting = input.Sorting.Trim(); | |
| 41 | + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase)) | |
| 42 | + { | |
| 43 | + query = query.OrderByDescending(x => x.OrderNum); | |
| 44 | + } | |
| 45 | + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase)) | |
| 46 | + { | |
| 47 | + query = query.OrderBy(x => x.OrderNum); | |
| 48 | + } | |
| 49 | + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) | |
| 50 | + { | |
| 51 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 52 | + } | |
| 53 | + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) | |
| 54 | + { | |
| 55 | + query = query.OrderBy(x => x.CreationTime); | |
| 56 | + } | |
| 57 | + else | |
| 58 | + { | |
| 59 | + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime); | |
| 60 | + } | |
| 39 | 61 | } |
| 40 | 62 | else |
| 41 | 63 | { |
| ... | ... | @@ -58,8 +80,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 58 | 80 | Id = x.Id, |
| 59 | 81 | CategoryCode = x.CategoryCode, |
| 60 | 82 | CategoryName = x.CategoryName, |
| 83 | + DisplayText = x.DisplayText, | |
| 61 | 84 | CategoryPhotoUrl = x.CategoryPhotoUrl, |
| 62 | 85 | State = x.State, |
| 86 | + ButtonAppearance = x.ButtonAppearance, | |
| 87 | + AvailabilityType = x.AvailabilityType, | |
| 63 | 88 | OrderNum = x.OrderNum, |
| 64 | 89 | NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, |
| 65 | 90 | LastEdited = x.LastModificationTime ?? x.CreationTime |
| ... | ... | @@ -77,7 +102,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 77 | 102 | throw new UserFriendlyException("标签分类不存在"); |
| 78 | 103 | } |
| 79 | 104 | |
| 80 | - return MapToGetOutput(entity); | |
| 105 | + var dto = MapToGetOutput(entity); | |
| 106 | + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | |
| 107 | + { | |
| 108 | + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>() | |
| 109 | + .Where(x => x.CategoryId == entity.Id) | |
| 110 | + .Select(x => x.LocationId) | |
| 111 | + .ToListAsync(); | |
| 112 | + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); | |
| 113 | + } | |
| 114 | + | |
| 115 | + return dto; | |
| 81 | 116 | } |
| 82 | 117 | |
| 83 | 118 | public async Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input) |
| ... | ... | @@ -89,6 +124,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 89 | 124 | throw new UserFriendlyException("分类编码和名称不能为空"); |
| 90 | 125 | } |
| 91 | 126 | |
| 127 | + var displayText = input.DisplayText?.Trim(); | |
| 128 | + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | |
| 129 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | |
| 130 | + var locationIds = NormalizeLocationIds(input.LocationIds); | |
| 131 | + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | |
| 132 | + | |
| 92 | 133 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 93 | 134 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| 94 | 135 | if (duplicated) |
| ... | ... | @@ -96,17 +137,23 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 96 | 137 | throw new UserFriendlyException("分类编码或名称已存在"); |
| 97 | 138 | } |
| 98 | 139 | |
| 140 | + var now = DateTime.Now; | |
| 141 | + var currentUserId = CurrentUser?.Id?.ToString(); | |
| 99 | 142 | var entity = new FlLabelCategoryDbEntity |
| 100 | 143 | { |
| 101 | 144 | Id = _guidGenerator.Create().ToString(), |
| 102 | 145 | CategoryCode = code, |
| 103 | 146 | CategoryName = name, |
| 104 | - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), | |
| 147 | + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, | |
| 148 | + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl), | |
| 105 | 149 | State = input.State, |
| 150 | + ButtonAppearance = appearance, | |
| 151 | + AvailabilityType = availabilityType, | |
| 106 | 152 | OrderNum = input.OrderNum |
| 107 | 153 | }; |
| 108 | 154 | |
| 109 | 155 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 156 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); | |
| 110 | 157 | return await GetAsync(entity.Id); |
| 111 | 158 | } |
| 112 | 159 | |
| ... | ... | @@ -126,6 +173,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 126 | 173 | throw new UserFriendlyException("分类编码和名称不能为空"); |
| 127 | 174 | } |
| 128 | 175 | |
| 176 | + var displayText = input.DisplayText?.Trim(); | |
| 177 | + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | |
| 178 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | |
| 179 | + var locationIds = NormalizeLocationIds(input.LocationIds); | |
| 180 | + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | |
| 181 | + | |
| 129 | 182 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 130 | 183 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| 131 | 184 | if (duplicated) |
| ... | ... | @@ -135,13 +188,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 135 | 188 | |
| 136 | 189 | entity.CategoryCode = code; |
| 137 | 190 | entity.CategoryName = name; |
| 138 | - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); | |
| 191 | + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText; | |
| 192 | + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl); | |
| 139 | 193 | entity.State = input.State; |
| 194 | + entity.ButtonAppearance = appearance; | |
| 195 | + entity.AvailabilityType = availabilityType; | |
| 140 | 196 | entity.OrderNum = input.OrderNum; |
| 141 | 197 | entity.LastModificationTime = DateTime.Now; |
| 142 | 198 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 143 | 199 | |
| 144 | 200 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 201 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); | |
| 145 | 202 | return await GetAsync(id); |
| 146 | 203 | } |
| 147 | 204 | |
| ... | ... | @@ -174,12 +231,70 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 174 | 231 | Id = x.Id, |
| 175 | 232 | CategoryCode = x.CategoryCode, |
| 176 | 233 | CategoryName = x.CategoryName, |
| 234 | + DisplayText = x.DisplayText, | |
| 177 | 235 | CategoryPhotoUrl = x.CategoryPhotoUrl, |
| 178 | 236 | State = x.State, |
| 237 | + ButtonAppearance = x.ButtonAppearance, | |
| 238 | + AvailabilityType = x.AvailabilityType, | |
| 179 | 239 | OrderNum = x.OrderNum |
| 180 | 240 | }; |
| 181 | 241 | } |
| 182 | 242 | |
| 243 | + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds) | |
| 244 | + { | |
| 245 | + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") | |
| 246 | + { | |
| 247 | + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | |
| 248 | + } | |
| 249 | + | |
| 250 | + if (availabilityType == "SPECIFIED" && locationIds.Count == 0) | |
| 251 | + { | |
| 252 | + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); | |
| 253 | + } | |
| 254 | + } | |
| 255 | + | |
| 256 | + private static List<string> NormalizeLocationIds(List<string>? locationIds) | |
| 257 | + { | |
| 258 | + return locationIds? | |
| 259 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 260 | + .Select(x => x.Trim()) | |
| 261 | + .Distinct() | |
| 262 | + .ToList() ?? new(); | |
| 263 | + } | |
| 264 | + | |
| 265 | + private async Task SaveCategoryLocationsAsync( | |
| 266 | + string categoryId, | |
| 267 | + string availabilityType, | |
| 268 | + List<string> locationIds, | |
| 269 | + string? currentUserId, | |
| 270 | + DateTime now) | |
| 271 | + { | |
| 272 | + await _dbContext.SqlSugarClient.Deleteable<FlLabelCategoryLocationDbEntity>() | |
| 273 | + .Where(x => x.CategoryId == categoryId) | |
| 274 | + .ExecuteCommandAsync(); | |
| 275 | + | |
| 276 | + if (availabilityType != "SPECIFIED") | |
| 277 | + { | |
| 278 | + return; | |
| 279 | + } | |
| 280 | + | |
| 281 | + if (locationIds.Count == 0) | |
| 282 | + { | |
| 283 | + return; | |
| 284 | + } | |
| 285 | + | |
| 286 | + var rows = locationIds.Select(locId => new FlLabelCategoryLocationDbEntity | |
| 287 | + { | |
| 288 | + Id = _guidGenerator.Create().ToString(), | |
| 289 | + CategoryId = categoryId, | |
| 290 | + LocationId = locId, | |
| 291 | + CreationTime = now, | |
| 292 | + CreatorId = currentUserId | |
| 293 | + }).ToList(); | |
| 294 | + | |
| 295 | + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | |
| 296 | + } | |
| 297 | + | |
| 183 | 298 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) |
| 184 | 299 | { |
| 185 | 300 | var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationSupportAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.LocationSupport; | |
| 3 | +using FoodLabeling.Application.Contracts.IServices; | |
| 4 | +using FoodLabeling.Application.Services.DbModels; | |
| 5 | +using Microsoft.AspNetCore.Authorization; | |
| 6 | +using Volo.Abp; | |
| 7 | +using Volo.Abp.Application.Services; | |
| 8 | +using Volo.Abp.Guids; | |
| 9 | +using Volo.Abp.Uow; | |
| 10 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 11 | + | |
| 12 | +namespace FoodLabeling.Application.Services; | |
| 13 | + | |
| 14 | +/// <summary> | |
| 15 | +/// 全局 Support 联系方式(全门店共用;Web 可增改查,App JWT 仅可读) | |
| 16 | +/// </summary> | |
| 17 | +[Authorize] | |
| 18 | +public class LocationSupportAppService : ApplicationService, ILocationSupportAppService | |
| 19 | +{ | |
| 20 | + private readonly ISqlSugarDbContext _dbContext; | |
| 21 | + private readonly IGuidGenerator _guidGenerator; | |
| 22 | + | |
| 23 | + public LocationSupportAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | |
| 24 | + { | |
| 25 | + _dbContext = dbContext; | |
| 26 | + _guidGenerator = guidGenerator; | |
| 27 | + } | |
| 28 | + | |
| 29 | + /// <inheritdoc /> | |
| 30 | + public async Task<LocationSupportGetOutputDto?> GetSupportAsync() | |
| 31 | + { | |
| 32 | + var rows = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>() | |
| 33 | + .Where(x => !x.IsDeleted) | |
| 34 | + .ToListAsync(); | |
| 35 | + var entity = rows.FirstOrDefault(); | |
| 36 | + return MapOutput(entity); | |
| 37 | + } | |
| 38 | + | |
| 39 | + /// <inheritdoc /> | |
| 40 | + [UnitOfWork] | |
| 41 | + public async Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input) | |
| 42 | + { | |
| 43 | + EnsureNotUsAppClient(); | |
| 44 | + | |
| 45 | + if (input is null) | |
| 46 | + { | |
| 47 | + throw new UserFriendlyException("Request body is required."); | |
| 48 | + } | |
| 49 | + | |
| 50 | + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required."); | |
| 51 | + var email = NormalizeRequired(input.SupportEmail, "Support email is required."); | |
| 52 | + EnsureEmailFormat(email); | |
| 53 | + | |
| 54 | + var existed = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>() | |
| 55 | + .AnyAsync(x => !x.IsDeleted); | |
| 56 | + if (existed) | |
| 57 | + { | |
| 58 | + throw new UserFriendlyException( | |
| 59 | + "Global support contact already exists. Use update instead."); | |
| 60 | + } | |
| 61 | + | |
| 62 | + var now = Clock.Now; | |
| 63 | + var entity = new FlLocationSupportDbEntity | |
| 64 | + { | |
| 65 | + Id = _guidGenerator.Create().ToString(), | |
| 66 | + IsDeleted = false, | |
| 67 | + CreationTime = now, | |
| 68 | + CreatorId = CurrentUser?.Id?.ToString(), | |
| 69 | + LastModificationTime = now, | |
| 70 | + LastModifierId = CurrentUser?.Id?.ToString(), | |
| 71 | + SupportPhone = phone, | |
| 72 | + SupportEmail = email | |
| 73 | + }; | |
| 74 | + | |
| 75 | + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | |
| 76 | + return MapOutput(entity)!; | |
| 77 | + } | |
| 78 | + | |
| 79 | + /// <inheritdoc /> | |
| 80 | + [UnitOfWork] | |
| 81 | + public async Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input) | |
| 82 | + { | |
| 83 | + EnsureNotUsAppClient(); | |
| 84 | + | |
| 85 | + var supportId = id?.Trim(); | |
| 86 | + if (string.IsNullOrWhiteSpace(supportId)) | |
| 87 | + { | |
| 88 | + throw new UserFriendlyException("Support record id is required."); | |
| 89 | + } | |
| 90 | + | |
| 91 | + if (input is null) | |
| 92 | + { | |
| 93 | + throw new UserFriendlyException("Request body is required."); | |
| 94 | + } | |
| 95 | + | |
| 96 | + var rows = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>() | |
| 97 | + .Where(x => !x.IsDeleted && x.Id == supportId) | |
| 98 | + .ToListAsync(); | |
| 99 | + var entity = rows.FirstOrDefault(); | |
| 100 | + if (entity is null) | |
| 101 | + { | |
| 102 | + throw new UserFriendlyException("Support record not found."); | |
| 103 | + } | |
| 104 | + | |
| 105 | + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required."); | |
| 106 | + var email = NormalizeRequired(input.SupportEmail, "Support email is required."); | |
| 107 | + EnsureEmailFormat(email); | |
| 108 | + | |
| 109 | + entity.SupportPhone = phone; | |
| 110 | + entity.SupportEmail = email; | |
| 111 | + entity.LastModificationTime = Clock.Now; | |
| 112 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 113 | + | |
| 114 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 115 | + return MapOutput(entity)!; | |
| 116 | + } | |
| 117 | + | |
| 118 | + private void EnsureNotUsAppClient() | |
| 119 | + { | |
| 120 | + if (CurrentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value == UsAppJwtClaims.ClientKindUsApp) | |
| 121 | + { | |
| 122 | + throw new UserFriendlyException( | |
| 123 | + "The mobile app can only view support contacts. Please use the web console to edit."); | |
| 124 | + } | |
| 125 | + } | |
| 126 | + | |
| 127 | + private static string NormalizeRequired(string? value, string message) | |
| 128 | + { | |
| 129 | + var normalized = value?.Trim(); | |
| 130 | + if (string.IsNullOrWhiteSpace(normalized)) | |
| 131 | + { | |
| 132 | + throw new UserFriendlyException(message); | |
| 133 | + } | |
| 134 | + | |
| 135 | + return normalized; | |
| 136 | + } | |
| 137 | + | |
| 138 | + private static void EnsureEmailFormat(string email) | |
| 139 | + { | |
| 140 | + if (!email.Contains("@", StringComparison.Ordinal) || email.StartsWith("@", StringComparison.Ordinal) || | |
| 141 | + email.EndsWith("@", StringComparison.Ordinal)) | |
| 142 | + { | |
| 143 | + throw new UserFriendlyException("Support email format is invalid."); | |
| 144 | + } | |
| 145 | + } | |
| 146 | + | |
| 147 | + private static LocationSupportGetOutputDto? MapOutput(FlLocationSupportDbEntity? entity) | |
| 148 | + { | |
| 149 | + if (entity is null) | |
| 150 | + { | |
| 151 | + return null; | |
| 152 | + } | |
| 153 | + | |
| 154 | + return new LocationSupportGetOutputDto | |
| 155 | + { | |
| 156 | + Id = entity.Id, | |
| 157 | + SupportPhone = entity.SupportPhone, | |
| 158 | + SupportEmail = entity.SupportEmail | |
| 159 | + }; | |
| 160 | + } | |
| 161 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Helpers; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 4 | +using FoodLabeling.Application.Contracts.IServices; | |
| 5 | +using FoodLabeling.Application.Services.DbModels; | |
| 6 | +using Microsoft.AspNetCore.Mvc; | |
| 7 | +using QuestPDF.Fluent; | |
| 8 | +using QuestPDF.Helpers; | |
| 9 | +using QuestPDF.Infrastructure; | |
| 10 | +using SqlSugar; | |
| 11 | +using Volo.Abp; | |
| 12 | +using Volo.Abp.Application.Services; | |
| 13 | +using Volo.Abp.Guids; | |
| 14 | +using Volo.Abp.Uow; | |
| 15 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 16 | + | |
| 17 | +namespace FoodLabeling.Application.Services; | |
| 18 | + | |
| 19 | +/// <summary> | |
| 20 | +/// 合作伙伴管理(fl_partner) | |
| 21 | +/// </summary> | |
| 22 | +public class PartnerAppService : ApplicationService, IPartnerAppService | |
| 23 | +{ | |
| 24 | + private const int ExportPdfMaxRows = 5000; | |
| 25 | + | |
| 26 | + private readonly ISqlSugarDbContext _dbContext; | |
| 27 | + private readonly IGuidGenerator _guidGenerator; | |
| 28 | + | |
| 29 | + public PartnerAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | |
| 30 | + { | |
| 31 | + _dbContext = dbContext; | |
| 32 | + _guidGenerator = guidGenerator; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /// <inheritdoc /> | |
| 36 | + public async Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input) | |
| 37 | + { | |
| 38 | + RefAsync<int> total = 0; | |
| 39 | + var query = BuildPartnerListQuery(input); | |
| 40 | + | |
| 41 | + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | |
| 42 | + var items = entities.Select(MapListItem).ToList(); | |
| 43 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | |
| 44 | + } | |
| 45 | + | |
| 46 | + /// <inheritdoc /> | |
| 47 | + public async Task<PartnerGetOutputDto> GetAsync(string id) | |
| 48 | + { | |
| 49 | + var partnerId = id?.Trim(); | |
| 50 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 51 | + { | |
| 52 | + throw new UserFriendlyException("合作伙伴Id不能为空"); | |
| 53 | + } | |
| 54 | + | |
| 55 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 56 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 57 | + if (entity is null) | |
| 58 | + { | |
| 59 | + throw new UserFriendlyException("合作伙伴不存在"); | |
| 60 | + } | |
| 61 | + | |
| 62 | + return MapDetail(entity); | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <inheritdoc /> | |
| 66 | + [UnitOfWork] | |
| 67 | + public async Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input) | |
| 68 | + { | |
| 69 | + var name = input.PartnerName?.Trim(); | |
| 70 | + if (string.IsNullOrWhiteSpace(name)) | |
| 71 | + { | |
| 72 | + throw new UserFriendlyException("合作伙伴名称不能为空"); | |
| 73 | + } | |
| 74 | + | |
| 75 | + var email = input.ContactEmail?.Trim(); | |
| 76 | + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) | |
| 77 | + { | |
| 78 | + throw new UserFriendlyException("联系邮箱格式不正确"); | |
| 79 | + } | |
| 80 | + | |
| 81 | + var now = Clock.Now; | |
| 82 | + var entity = new FlPartnerDbEntity | |
| 83 | + { | |
| 84 | + Id = _guidGenerator.Create().ToString(), | |
| 85 | + IsDeleted = false, | |
| 86 | + PartnerName = name, | |
| 87 | + ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email, | |
| 88 | + PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(), | |
| 89 | + State = input.State, | |
| 90 | + CreationTime = now, | |
| 91 | + CreatorId = CurrentUser?.Id?.ToString(), | |
| 92 | + LastModificationTime = now, | |
| 93 | + LastModifierId = CurrentUser?.Id?.ToString() | |
| 94 | + }; | |
| 95 | + | |
| 96 | + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | |
| 97 | + return await GetAsync(entity.Id); | |
| 98 | + } | |
| 99 | + | |
| 100 | + /// <inheritdoc /> | |
| 101 | + [UnitOfWork] | |
| 102 | + public async Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input) | |
| 103 | + { | |
| 104 | + var partnerId = id?.Trim(); | |
| 105 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 106 | + { | |
| 107 | + throw new UserFriendlyException("合作伙伴Id不能为空"); | |
| 108 | + } | |
| 109 | + | |
| 110 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 111 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 112 | + if (entity is null) | |
| 113 | + { | |
| 114 | + throw new UserFriendlyException("合作伙伴不存在"); | |
| 115 | + } | |
| 116 | + | |
| 117 | + var name = input.PartnerName?.Trim(); | |
| 118 | + if (string.IsNullOrWhiteSpace(name)) | |
| 119 | + { | |
| 120 | + throw new UserFriendlyException("合作伙伴名称不能为空"); | |
| 121 | + } | |
| 122 | + | |
| 123 | + var email = input.ContactEmail?.Trim(); | |
| 124 | + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) | |
| 125 | + { | |
| 126 | + throw new UserFriendlyException("联系邮箱格式不正确"); | |
| 127 | + } | |
| 128 | + | |
| 129 | + entity.PartnerName = name; | |
| 130 | + entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email; | |
| 131 | + entity.PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(); | |
| 132 | + entity.State = input.State; | |
| 133 | + entity.LastModificationTime = Clock.Now; | |
| 134 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 135 | + | |
| 136 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 137 | + return await GetAsync(partnerId); | |
| 138 | + } | |
| 139 | + | |
| 140 | + /// <inheritdoc /> | |
| 141 | + [UnitOfWork] | |
| 142 | + public async Task DeleteAsync(string id) | |
| 143 | + { | |
| 144 | + var partnerId = id?.Trim(); | |
| 145 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 146 | + { | |
| 147 | + throw new UserFriendlyException("合作伙伴Id不能为空"); | |
| 148 | + } | |
| 149 | + | |
| 150 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 151 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 152 | + if (entity is null) | |
| 153 | + { | |
| 154 | + throw new UserFriendlyException("合作伙伴不存在"); | |
| 155 | + } | |
| 156 | + | |
| 157 | + entity.IsDeleted = true; | |
| 158 | + entity.LastModificationTime = Clock.Now; | |
| 159 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 160 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 161 | + } | |
| 162 | + | |
| 163 | + /// <inheritdoc /> | |
| 164 | + public async Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input) | |
| 165 | + { | |
| 166 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 167 | + | |
| 168 | + var count = await BuildPartnerListQuery(input).CountAsync(); | |
| 169 | + var query = BuildPartnerListQuery(input); | |
| 170 | + if (count > ExportPdfMaxRows) | |
| 171 | + { | |
| 172 | + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | |
| 173 | + } | |
| 174 | + | |
| 175 | + var rows = await query.Take(ExportPdfMaxRows).ToListAsync(); | |
| 176 | + var fileName = $"partners_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 177 | + | |
| 178 | + var document = Document.Create(container => | |
| 179 | + { | |
| 180 | + container.Page(page => | |
| 181 | + { | |
| 182 | + page.Margin(28); | |
| 183 | + page.DefaultTextStyle(x => x.FontSize(10)); | |
| 184 | + page.Header().Text("Partners").SemiBold().FontSize(18); | |
| 185 | + page.Content().PaddingTop(12).Table(table => | |
| 186 | + { | |
| 187 | + table.ColumnsDefinition(c => | |
| 188 | + { | |
| 189 | + c.RelativeColumn(2.2f); | |
| 190 | + c.RelativeColumn(2.4f); | |
| 191 | + c.RelativeColumn(1.8f); | |
| 192 | + c.RelativeColumn(1f); | |
| 193 | + c.RelativeColumn(1.6f); | |
| 194 | + }); | |
| 195 | + | |
| 196 | + static IContainer CellHeader(IContainer c) => | |
| 197 | + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold()); | |
| 198 | + | |
| 199 | + table.Cell().Element(CellHeader).Text("Partner"); | |
| 200 | + table.Cell().Element(CellHeader).Text("Contact"); | |
| 201 | + table.Cell().Element(CellHeader).Text("Phone"); | |
| 202 | + table.Cell().Element(CellHeader).Text("Status"); | |
| 203 | + table.Cell().Element(CellHeader).Text("Created"); | |
| 204 | + | |
| 205 | + foreach (var e in rows) | |
| 206 | + { | |
| 207 | + var status = e.State ? "active" : "inactive"; | |
| 208 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 209 | + .Text(e.PartnerName ?? string.Empty); | |
| 210 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 211 | + .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "无" : e.ContactEmail!); | |
| 212 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 213 | + .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "无" : e.PhoneNumber!); | |
| 214 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); | |
| 215 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 216 | + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); | |
| 217 | + } | |
| 218 | + }); | |
| 219 | + }); | |
| 220 | + }); | |
| 221 | + | |
| 222 | + var stream = new MemoryStream(); | |
| 223 | + document.GeneratePdf(stream); | |
| 224 | + stream.Position = 0; | |
| 225 | + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; | |
| 226 | + } | |
| 227 | + | |
| 228 | + private ISugarQueryable<FlPartnerDbEntity> BuildPartnerListQuery(PartnerGetListInputVo input) | |
| 229 | + { | |
| 230 | + var keyword = input.Keyword?.Trim(); | |
| 231 | + var query = _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 232 | + .Where(x => !x.IsDeleted) | |
| 233 | + .WhereIF(input.State != null, x => x.State == input.State) | |
| 234 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), | |
| 235 | + x => x.PartnerName.Contains(keyword!) || | |
| 236 | + (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) || | |
| 237 | + (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!))); | |
| 238 | + | |
| 239 | + if (!string.IsNullOrWhiteSpace(input.Sorting)) | |
| 240 | + { | |
| 241 | + var sorting = input.Sorting.Trim(); | |
| 242 | + if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase)) | |
| 243 | + { | |
| 244 | + query = query.OrderByDescending(x => x.PartnerName); | |
| 245 | + } | |
| 246 | + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase)) | |
| 247 | + { | |
| 248 | + query = query.OrderBy(x => x.PartnerName); | |
| 249 | + } | |
| 250 | + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) | |
| 251 | + { | |
| 252 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 253 | + } | |
| 254 | + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) | |
| 255 | + { | |
| 256 | + query = query.OrderBy(x => x.CreationTime); | |
| 257 | + } | |
| 258 | + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase)) | |
| 259 | + { | |
| 260 | + query = query.OrderByDescending(x => x.State); | |
| 261 | + } | |
| 262 | + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase)) | |
| 263 | + { | |
| 264 | + query = query.OrderBy(x => x.State); | |
| 265 | + } | |
| 266 | + else | |
| 267 | + { | |
| 268 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 269 | + } | |
| 270 | + } | |
| 271 | + else | |
| 272 | + { | |
| 273 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 274 | + } | |
| 275 | + | |
| 276 | + return query; | |
| 277 | + } | |
| 278 | + | |
| 279 | + private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) => new() | |
| 280 | + { | |
| 281 | + Id = x.Id, | |
| 282 | + PartnerName = x.PartnerName, | |
| 283 | + ContactEmail = x.ContactEmail, | |
| 284 | + PhoneNumber = x.PhoneNumber, | |
| 285 | + State = x.State, | |
| 286 | + CreationTime = x.CreationTime | |
| 287 | + }; | |
| 288 | + | |
| 289 | + private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) => new() | |
| 290 | + { | |
| 291 | + Id = x.Id, | |
| 292 | + PartnerName = x.PartnerName, | |
| 293 | + ContactEmail = x.ContactEmail, | |
| 294 | + PhoneNumber = x.PhoneNumber, | |
| 295 | + State = x.State, | |
| 296 | + CreationTime = x.CreationTime, | |
| 297 | + LastModificationTime = x.LastModificationTime | |
| 298 | + }; | |
| 299 | + | |
| 300 | + private static bool IsPlausibleEmail(string email) | |
| 301 | + { | |
| 302 | + if (email.Length > 256) | |
| 303 | + { | |
| 304 | + return false; | |
| 305 | + } | |
| 306 | + | |
| 307 | + var at = email.IndexOf('@'); | |
| 308 | + return at > 0 && at < email.Length - 1 && email.IndexOf('@', at + 1) < 0; | |
| 309 | + } | |
| 310 | + | |
| 311 | + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, | |
| 312 | + List<T> items) | |
| 313 | + { | |
| 314 | + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | |
| 315 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); | |
| 316 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | |
| 317 | + return new PagedResultWithPageDto<T> | |
| 318 | + { | |
| 319 | + PageIndex = pageIndex, | |
| 320 | + PageSize = pageSize, | |
| 321 | + TotalCount = total, | |
| 322 | + TotalPages = totalPages, | |
| 323 | + Items = items | |
| 324 | + }; | |
| 325 | + } | |
| 326 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
| ... | ... | @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; |
| 3 | 3 | using FoodLabeling.Application.Contracts.Dtos.Product; |
| 4 | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 5 | 5 | using FoodLabeling.Application.Services.DbModels; |
| 6 | +using FoodLabeling.Domain.Entities; | |
| 6 | 7 | using SqlSugar; |
| 7 | 8 | using Volo.Abp; |
| 8 | 9 | using Volo.Abp.Application.Services; |
| ... | ... | @@ -188,6 +189,13 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 188 | 189 | }; |
| 189 | 190 | |
| 190 | 191 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 192 | + | |
| 193 | + if (input.LocationIds is not null) | |
| 194 | + { | |
| 195 | + var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); | |
| 196 | + await ReplaceProductLocationLinksAsync(entity.Id, locIds); | |
| 197 | + } | |
| 198 | + | |
| 191 | 199 | return await GetAsync(entity.Id); |
| 192 | 200 | } |
| 193 | 201 | |
| ... | ... | @@ -228,6 +236,13 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 228 | 236 | entity.State = input.State; |
| 229 | 237 | |
| 230 | 238 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 239 | + | |
| 240 | + if (input.LocationIds is not null) | |
| 241 | + { | |
| 242 | + var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); | |
| 243 | + await ReplaceProductLocationLinksAsync(productId, locIds); | |
| 244 | + } | |
| 245 | + | |
| 231 | 246 | return await GetAsync(productId); |
| 232 | 247 | } |
| 233 | 248 | |
| ... | ... | @@ -251,6 +266,66 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 251 | 266 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 252 | 267 | } |
| 253 | 268 | |
| 269 | + /// <summary> | |
| 270 | + /// 去重、校验门店 Id 格式与存在性。 | |
| 271 | + /// </summary> | |
| 272 | + private async Task<List<string>> NormalizeAndValidateLocationIdsAsync(IEnumerable<string> rawIds) | |
| 273 | + { | |
| 274 | + var distinct = rawIds | |
| 275 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 276 | + .Select(x => x.Trim()) | |
| 277 | + .Distinct(StringComparer.Ordinal) | |
| 278 | + .ToList(); | |
| 279 | + | |
| 280 | + if (distinct.Count == 0) | |
| 281 | + { | |
| 282 | + return new List<string>(); | |
| 283 | + } | |
| 284 | + | |
| 285 | + foreach (var id in distinct) | |
| 286 | + { | |
| 287 | + if (!Guid.TryParse(id, out _)) | |
| 288 | + { | |
| 289 | + throw new UserFriendlyException("门店Id格式不正确"); | |
| 290 | + } | |
| 291 | + } | |
| 292 | + | |
| 293 | + var guidList = distinct.Select(Guid.Parse).ToList(); | |
| 294 | + var existCount = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | |
| 295 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | |
| 296 | + .CountAsync(); | |
| 297 | + if (existCount != distinct.Count) | |
| 298 | + { | |
| 299 | + throw new UserFriendlyException("门店不存在"); | |
| 300 | + } | |
| 301 | + | |
| 302 | + return distinct; | |
| 303 | + } | |
| 304 | + | |
| 305 | + /// <summary> | |
| 306 | + /// 按产品维度替换 fl_location_product:先删本产品全部关联,再按列表插入(每门店一行)。 | |
| 307 | + /// </summary> | |
| 308 | + private async Task ReplaceProductLocationLinksAsync(string productId, List<string> locationIds) | |
| 309 | + { | |
| 310 | + await _dbContext.SqlSugarClient.Deleteable<FlLocationProductDbEntity>() | |
| 311 | + .Where(x => x.ProductId == productId) | |
| 312 | + .ExecuteCommandAsync(); | |
| 313 | + | |
| 314 | + if (locationIds.Count == 0) | |
| 315 | + { | |
| 316 | + return; | |
| 317 | + } | |
| 318 | + | |
| 319 | + var rows = locationIds.Select(lid => new FlLocationProductDbEntity | |
| 320 | + { | |
| 321 | + Id = _guidGenerator.Create().ToString(), | |
| 322 | + LocationId = lid, | |
| 323 | + ProductId = productId | |
| 324 | + }).ToList(); | |
| 325 | + | |
| 326 | + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | |
| 327 | + } | |
| 328 | + | |
| 254 | 329 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) |
| 255 | 330 | { |
| 256 | 331 | var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
| 1 | +using FoodLabeling.Application.Helpers; | |
| 1 | 2 | using FoodLabeling.Application.Contracts.Dtos.Common; |
| 2 | 3 | using FoodLabeling.Application.Contracts.Dtos.ProductCategory; |
| 3 | 4 | using FoodLabeling.Application.Contracts.IServices; |
| ... | ... | @@ -35,7 +36,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 35 | 36 | var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 36 | 37 | .Where(x => !x.IsDeleted) |
| 37 | 38 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 38 | - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) | |
| 39 | + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) || | |
| 40 | + (x.DisplayText != null && x.DisplayText.Contains(keyword!))) | |
| 39 | 41 | .WhereIF(input.State != null, x => x.State == input.State); |
| 40 | 42 | |
| 41 | 43 | // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column |
| ... | ... | @@ -77,8 +79,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 77 | 79 | Id = x.Id, |
| 78 | 80 | CategoryCode = x.CategoryCode, |
| 79 | 81 | CategoryName = x.CategoryName, |
| 82 | + DisplayText = x.DisplayText, | |
| 80 | 83 | CategoryPhotoUrl = x.CategoryPhotoUrl, |
| 84 | + ButtonAppearance = x.ButtonAppearance, | |
| 81 | 85 | State = x.State, |
| 86 | + AvailabilityType = x.AvailabilityType, | |
| 82 | 87 | OrderNum = x.OrderNum, |
| 83 | 88 | LastEdited = x.LastModificationTime ?? x.CreationTime |
| 84 | 89 | }).ToList(); |
| ... | ... | @@ -98,7 +103,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 98 | 103 | throw new UserFriendlyException("类别不存在"); |
| 99 | 104 | } |
| 100 | 105 | |
| 101 | - return MapToGetOutput(entity); | |
| 106 | + var dto = MapToGetOutput(entity); | |
| 107 | + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | |
| 108 | + { | |
| 109 | + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryLocationDbEntity>() | |
| 110 | + .Where(x => x.CategoryId == entity.Id) | |
| 111 | + .Select(x => x.LocationId) | |
| 112 | + .ToListAsync(); | |
| 113 | + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); | |
| 114 | + } | |
| 115 | + | |
| 116 | + return dto; | |
| 102 | 117 | } |
| 103 | 118 | |
| 104 | 119 | /// <summary> |
| ... | ... | @@ -113,6 +128,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 113 | 128 | throw new UserFriendlyException("类别编码和名称不能为空"); |
| 114 | 129 | } |
| 115 | 130 | |
| 131 | + var displayText = input.DisplayText?.Trim(); | |
| 132 | + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | |
| 133 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | |
| 134 | + var locationIds = NormalizeLocationIds(input.LocationIds); | |
| 135 | + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | |
| 136 | + | |
| 116 | 137 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 117 | 138 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| 118 | 139 | if (duplicated) |
| ... | ... | @@ -133,12 +154,16 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 133 | 154 | ConcurrencyStamp = _guidGenerator.Create().ToString("N"), |
| 134 | 155 | CategoryCode = code, |
| 135 | 156 | CategoryName = name, |
| 136 | - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), | |
| 157 | + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, | |
| 158 | + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl), | |
| 159 | + ButtonAppearance = appearance, | |
| 137 | 160 | State = input.State, |
| 161 | + AvailabilityType = availabilityType, | |
| 138 | 162 | OrderNum = input.OrderNum |
| 139 | 163 | }; |
| 140 | 164 | |
| 141 | 165 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 166 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); | |
| 142 | 167 | return await GetAsync(entity.Id); |
| 143 | 168 | } |
| 144 | 169 | |
| ... | ... | @@ -161,6 +186,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 161 | 186 | throw new UserFriendlyException("类别编码和名称不能为空"); |
| 162 | 187 | } |
| 163 | 188 | |
| 189 | + var displayText = input.DisplayText?.Trim(); | |
| 190 | + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | |
| 191 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | |
| 192 | + var locationIds = NormalizeLocationIds(input.LocationIds); | |
| 193 | + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | |
| 194 | + | |
| 164 | 195 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 165 | 196 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| 166 | 197 | if (duplicated) |
| ... | ... | @@ -170,13 +201,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 170 | 201 | |
| 171 | 202 | entity.CategoryCode = code; |
| 172 | 203 | entity.CategoryName = name; |
| 173 | - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); | |
| 204 | + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText; | |
| 205 | + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl); | |
| 206 | + entity.ButtonAppearance = appearance; | |
| 174 | 207 | entity.State = input.State; |
| 208 | + entity.AvailabilityType = availabilityType; | |
| 175 | 209 | entity.OrderNum = input.OrderNum; |
| 176 | 210 | entity.LastModificationTime = DateTime.Now; |
| 177 | 211 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 178 | 212 | |
| 179 | 213 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 214 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); | |
| 180 | 215 | return await GetAsync(id); |
| 181 | 216 | } |
| 182 | 217 | |
| ... | ... | @@ -213,12 +248,70 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 213 | 248 | Id = x.Id, |
| 214 | 249 | CategoryCode = x.CategoryCode, |
| 215 | 250 | CategoryName = x.CategoryName, |
| 251 | + DisplayText = x.DisplayText, | |
| 216 | 252 | CategoryPhotoUrl = x.CategoryPhotoUrl, |
| 253 | + ButtonAppearance = x.ButtonAppearance, | |
| 217 | 254 | State = x.State, |
| 255 | + AvailabilityType = x.AvailabilityType, | |
| 218 | 256 | OrderNum = x.OrderNum |
| 219 | 257 | }; |
| 220 | 258 | } |
| 221 | 259 | |
| 260 | + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds) | |
| 261 | + { | |
| 262 | + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") | |
| 263 | + { | |
| 264 | + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | |
| 265 | + } | |
| 266 | + | |
| 267 | + if (availabilityType == "SPECIFIED" && locationIds.Count == 0) | |
| 268 | + { | |
| 269 | + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); | |
| 270 | + } | |
| 271 | + } | |
| 272 | + | |
| 273 | + private static List<string> NormalizeLocationIds(List<string>? locationIds) | |
| 274 | + { | |
| 275 | + return locationIds? | |
| 276 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 277 | + .Select(x => x.Trim()) | |
| 278 | + .Distinct() | |
| 279 | + .ToList() ?? new(); | |
| 280 | + } | |
| 281 | + | |
| 282 | + private async Task SaveCategoryLocationsAsync( | |
| 283 | + string categoryId, | |
| 284 | + string availabilityType, | |
| 285 | + List<string> locationIds, | |
| 286 | + string? currentUserId, | |
| 287 | + DateTime now) | |
| 288 | + { | |
| 289 | + await _dbContext.SqlSugarClient.Deleteable<FlProductCategoryLocationDbEntity>() | |
| 290 | + .Where(x => x.CategoryId == categoryId) | |
| 291 | + .ExecuteCommandAsync(); | |
| 292 | + | |
| 293 | + if (availabilityType != "SPECIFIED") | |
| 294 | + { | |
| 295 | + return; | |
| 296 | + } | |
| 297 | + | |
| 298 | + if (locationIds.Count == 0) | |
| 299 | + { | |
| 300 | + return; | |
| 301 | + } | |
| 302 | + | |
| 303 | + var rows = locationIds.Select(locId => new FlProductCategoryLocationDbEntity | |
| 304 | + { | |
| 305 | + Id = _guidGenerator.Create().ToString(), | |
| 306 | + CategoryId = categoryId, | |
| 307 | + LocationId = locId, | |
| 308 | + CreationTime = now, | |
| 309 | + CreatorId = currentUserId | |
| 310 | + }).ToList(); | |
| 311 | + | |
| 312 | + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | |
| 313 | + } | |
| 314 | + | |
| 222 | 315 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) |
| 223 | 316 | { |
| 224 | 317 | var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacMenuAppService.cs
| ... | ... | @@ -38,6 +38,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService |
| 38 | 38 | Id = x.Id, |
| 39 | 39 | ParentId = x.ParentId, |
| 40 | 40 | MenuName = x.MenuName ?? string.Empty, |
| 41 | + RouterName = x.RouterName, | |
| 42 | + Router = x.Router, | |
| 41 | 43 | PermissionCode = x.PermissionCode, |
| 42 | 44 | MenuType = x.MenuType, |
| 43 | 45 | MenuSource = x.MenuSource, |
| ... | ... | @@ -62,6 +64,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService |
| 62 | 64 | Id = entity.Id, |
| 63 | 65 | ParentId = entity.ParentId, |
| 64 | 66 | MenuName = entity.MenuName ?? string.Empty, |
| 67 | + RouterName = entity.RouterName, | |
| 68 | + Router = entity.Router, | |
| 65 | 69 | PermissionCode = entity.PermissionCode, |
| 66 | 70 | MenuType = entity.MenuType, |
| 67 | 71 | MenuSource = entity.MenuSource, | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs
0 → 100644
| 1 | +using System.Globalization; | |
| 2 | +using System.Text.Json; | |
| 3 | +using FoodLabeling.Application.Helpers; | |
| 4 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 5 | +using FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 6 | +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | |
| 7 | +using FoodLabeling.Application.Contracts.IServices; | |
| 8 | +using FoodLabeling.Application.Services.DbModels; | |
| 9 | +using FoodLabeling.Domain.Entities; | |
| 10 | +using Microsoft.AspNetCore.Authorization; | |
| 11 | +using Microsoft.AspNetCore.Mvc; | |
| 12 | +using QuestPDF.Fluent; | |
| 13 | +using QuestPDF.Helpers; | |
| 14 | +using QuestPDF.Infrastructure; | |
| 15 | +using SqlSugar; | |
| 16 | +using Volo.Abp; | |
| 17 | +using Volo.Abp.Application.Services; | |
| 18 | +using Yi.Framework.Rbac.Domain.Entities; | |
| 19 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 20 | + | |
| 21 | +namespace FoodLabeling.Application.Services; | |
| 22 | + | |
| 23 | +/// <summary> | |
| 24 | +/// Reports(Print Log / Label Report) | |
| 25 | +/// </summary> | |
| 26 | +[Authorize] | |
| 27 | +public class ReportsAppService : ApplicationService, IReportsAppService | |
| 28 | +{ | |
| 29 | + private const int ExportPdfMaxRows = 5000; | |
| 30 | + | |
| 31 | + private readonly ISqlSugarDbContext _dbContext; | |
| 32 | + private readonly IUsAppLabelingAppService _usAppLabelingAppService; | |
| 33 | + | |
| 34 | + public ReportsAppService(ISqlSugarDbContext dbContext, IUsAppLabelingAppService usAppLabelingAppService) | |
| 35 | + { | |
| 36 | + _dbContext = dbContext; | |
| 37 | + _usAppLabelingAppService = usAppLabelingAppService; | |
| 38 | + } | |
| 39 | + | |
| 40 | + /// <inheritdoc /> | |
| 41 | + public async Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync( | |
| 42 | + ReportsPrintLogGetListInputVo input) | |
| 43 | + { | |
| 44 | + if (input is null) | |
| 45 | + { | |
| 46 | + throw new UserFriendlyException("入参不能为空"); | |
| 47 | + } | |
| 48 | + | |
| 49 | + if (!CurrentUser.Id.HasValue) | |
| 50 | + { | |
| 51 | + throw new UserFriendlyException("用户未登录"); | |
| 52 | + } | |
| 53 | + | |
| 54 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 55 | + if (locationIds is not null && locationIds.Count == 0) | |
| 56 | + { | |
| 57 | + return EmptyPrintLogPage(input); | |
| 58 | + } | |
| 59 | + | |
| 60 | + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 61 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 62 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 63 | + var keyword = input.Keyword?.Trim(); | |
| 64 | + | |
| 65 | + RefAsync<int> total = 0; | |
| 66 | + | |
| 67 | + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 68 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) | |
| 69 | + .Where((t, l, p, lc, pc, loc, tpl) => | |
| 70 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && | |
| 71 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl); | |
| 72 | + | |
| 73 | + if (!string.IsNullOrWhiteSpace(input.Sorting) && | |
| 74 | + input.Sorting.Trim().Equals("PrintedAt asc", StringComparison.OrdinalIgnoreCase)) | |
| 75 | + { | |
| 76 | + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 77 | + OrderByType.Asc); | |
| 78 | + } | |
| 79 | + else | |
| 80 | + { | |
| 81 | + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 82 | + OrderByType.Desc); | |
| 83 | + } | |
| 84 | + | |
| 85 | + var pageRows = await query | |
| 86 | + .Select((t, l, p, lc, pc, loc, tpl) => new | |
| 87 | + { | |
| 88 | + t.Id, | |
| 89 | + LabelCode = l.LabelCode, | |
| 90 | + ProductName = p.ProductName, | |
| 91 | + LabelCategoryName = lc.CategoryName, | |
| 92 | + ProductCategoryName = pc.CategoryName, | |
| 93 | + tpl.Width, | |
| 94 | + tpl.Height, | |
| 95 | + tpl.Unit, | |
| 96 | + tpl.TemplateName, | |
| 97 | + t.PrintInputJson, | |
| 98 | + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 99 | + t.CreatedBy, | |
| 100 | + t.LocationId, | |
| 101 | + LocName = loc.LocationName, | |
| 102 | + LocCode = loc.LocationCode | |
| 103 | + }) | |
| 104 | + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | |
| 105 | + | |
| 106 | + var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 107 | + .Select(x => x!).Distinct().ToList()); | |
| 108 | + | |
| 109 | + var items = pageRows.Select(x => | |
| 110 | + { | |
| 111 | + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) | |
| 112 | + ? x.ProductCategoryName!.Trim() | |
| 113 | + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); | |
| 114 | + var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); | |
| 115 | + var locText = FormatLocationText(x.LocName, x.LocCode); | |
| 116 | + var printedAt = x.PrintedAt ?? DateTime.MinValue; | |
| 117 | + return new ReportsPrintLogListItemDto | |
| 118 | + { | |
| 119 | + TaskId = x.Id, | |
| 120 | + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), | |
| 121 | + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), | |
| 122 | + CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, | |
| 123 | + TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, | |
| 124 | + PrintedAt = printedAt, | |
| 125 | + PrintedByName = ResolveUserName(userMap, x.CreatedBy), | |
| 126 | + LocationText = locText, | |
| 127 | + LocationId = x.LocationId?.Trim(), | |
| 128 | + ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) | |
| 129 | + }; | |
| 130 | + }).ToList(); | |
| 131 | + | |
| 132 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /// <inheritdoc /> | |
| 136 | + public async Task<IActionResult> ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input) | |
| 137 | + { | |
| 138 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 139 | + if (input is null) | |
| 140 | + { | |
| 141 | + throw new UserFriendlyException("入参不能为空"); | |
| 142 | + } | |
| 143 | + | |
| 144 | + if (!CurrentUser.Id.HasValue) | |
| 145 | + { | |
| 146 | + throw new UserFriendlyException("用户未登录"); | |
| 147 | + } | |
| 148 | + | |
| 149 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 150 | + if (locationIds is not null && locationIds.Count == 0) | |
| 151 | + { | |
| 152 | + return BuildEmptyPdf("print-log-empty.pdf"); | |
| 153 | + } | |
| 154 | + | |
| 155 | + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 156 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 157 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 158 | + var keyword = input.Keyword?.Trim(); | |
| 159 | + | |
| 160 | + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 161 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) | |
| 162 | + .Where((t, l, p, lc, pc, loc, tpl) => | |
| 163 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && | |
| 164 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) | |
| 165 | + .OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc); | |
| 166 | + | |
| 167 | + var count = await query.CountAsync(); | |
| 168 | + if (count > ExportPdfMaxRows) | |
| 169 | + { | |
| 170 | + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | |
| 171 | + } | |
| 172 | + | |
| 173 | + var rows = await query.Take(ExportPdfMaxRows) | |
| 174 | + .Select((t, l, p, lc, pc, loc, tpl) => new | |
| 175 | + { | |
| 176 | + t.Id, | |
| 177 | + LabelCode = l.LabelCode, | |
| 178 | + ProductName = p.ProductName, | |
| 179 | + LabelCategoryName = lc.CategoryName, | |
| 180 | + ProductCategoryName = pc.CategoryName, | |
| 181 | + tpl.Width, | |
| 182 | + tpl.Height, | |
| 183 | + tpl.Unit, | |
| 184 | + tpl.TemplateName, | |
| 185 | + t.PrintInputJson, | |
| 186 | + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 187 | + t.CreatedBy, | |
| 188 | + LocName = loc.LocationName, | |
| 189 | + LocCode = loc.LocationCode | |
| 190 | + }) | |
| 191 | + .ToListAsync(); | |
| 192 | + | |
| 193 | + var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 194 | + .Select(x => x!).Distinct().ToList()); | |
| 195 | + | |
| 196 | + var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 197 | + var document = Document.Create(container => | |
| 198 | + { | |
| 199 | + container.Page(page => | |
| 200 | + { | |
| 201 | + page.Margin(22); | |
| 202 | + page.DefaultTextStyle(x => x.FontSize(8.5f)); | |
| 203 | + page.Header().Text("Print Log").SemiBold().FontSize(16); | |
| 204 | + page.Content().PaddingTop(10).Table(table => | |
| 205 | + { | |
| 206 | + table.ColumnsDefinition(c => | |
| 207 | + { | |
| 208 | + c.RelativeColumn(1.1f); | |
| 209 | + c.RelativeColumn(1.2f); | |
| 210 | + c.RelativeColumn(0.9f); | |
| 211 | + c.RelativeColumn(1.1f); | |
| 212 | + c.RelativeColumn(1f); | |
| 213 | + c.RelativeColumn(0.9f); | |
| 214 | + c.RelativeColumn(0.9f); | |
| 215 | + c.RelativeColumn(0.8f); | |
| 216 | + }); | |
| 217 | + static IContainer H(IContainer x) => | |
| 218 | + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); | |
| 219 | + table.Cell().Element(H).Text("Label ID"); | |
| 220 | + table.Cell().Element(H).Text("Product"); | |
| 221 | + table.Cell().Element(H).Text("Category"); | |
| 222 | + table.Cell().Element(H).Text("Template"); | |
| 223 | + table.Cell().Element(H).Text("Printed At"); | |
| 224 | + table.Cell().Element(H).Text("Printed By"); | |
| 225 | + table.Cell().Element(H).Text("Location"); | |
| 226 | + table.Cell().Element(H).Text("Expiry"); | |
| 227 | + | |
| 228 | + foreach (var x in rows) | |
| 229 | + { | |
| 230 | + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) | |
| 231 | + ? x.ProductCategoryName!.Trim() | |
| 232 | + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); | |
| 233 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 234 | + .Text(string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim()); | |
| 235 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 236 | + .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); | |
| 237 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); | |
| 238 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 239 | + .Text(FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName)); | |
| 240 | + var printedAt = x.PrintedAt ?? DateTime.MinValue; | |
| 241 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 242 | + .Text(printedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); | |
| 243 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 244 | + .Text(ResolveUserName(userMap, x.CreatedBy)); | |
| 245 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 246 | + .Text(FormatLocationText(x.LocName, x.LocCode)); | |
| 247 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 248 | + .Text(TryExtractExpiryText(x.PrintInputJson)); | |
| 249 | + } | |
| 250 | + }); | |
| 251 | + }); | |
| 252 | + }); | |
| 253 | + | |
| 254 | + var ms = new MemoryStream(); | |
| 255 | + document.GeneratePdf(ms); | |
| 256 | + ms.Position = 0; | |
| 257 | + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; | |
| 258 | + } | |
| 259 | + | |
| 260 | + /// <inheritdoc /> | |
| 261 | + public Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input) => | |
| 262 | + _usAppLabelingAppService.ReprintAsync(input); | |
| 263 | + | |
| 264 | + /// <inheritdoc /> | |
| 265 | + public async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input) | |
| 266 | + { | |
| 267 | + if (input is null) | |
| 268 | + { | |
| 269 | + throw new UserFriendlyException("入参不能为空"); | |
| 270 | + } | |
| 271 | + | |
| 272 | + if (!CurrentUser.Id.HasValue) | |
| 273 | + { | |
| 274 | + throw new UserFriendlyException("用户未登录"); | |
| 275 | + } | |
| 276 | + | |
| 277 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 278 | + if (locationIds is not null && locationIds.Count == 0) | |
| 279 | + { | |
| 280 | + return new ReportsLabelReportOutputDto(); | |
| 281 | + } | |
| 282 | + | |
| 283 | + var (curStart, curEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 284 | + var span = curEndExcl - curStart; | |
| 285 | + if (span.TotalDays < 1) | |
| 286 | + { | |
| 287 | + span = TimeSpan.FromDays(1); | |
| 288 | + } | |
| 289 | + | |
| 290 | + var prevEndExcl = curStart; | |
| 291 | + var prevStart = curStart - span; | |
| 292 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 293 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 294 | + var keyword = input.Keyword?.Trim(); | |
| 295 | + | |
| 296 | + var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 297 | + .Where((t, l, p, lc, pc, loc) => | |
| 298 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 299 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 300 | + .CountAsync(); | |
| 301 | + | |
| 302 | + var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 303 | + .Where((t, l, p, lc, pc, loc) => | |
| 304 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && | |
| 305 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) | |
| 306 | + .CountAsync(); | |
| 307 | + | |
| 308 | + var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays)); | |
| 309 | + var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays)); | |
| 310 | + var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); | |
| 311 | + var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); | |
| 312 | + | |
| 313 | + var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 314 | + .Where((t, l, p, lc, pc, loc) => | |
| 315 | + l.LabelCategoryId != null && | |
| 316 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 317 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 318 | + .GroupBy((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName }) | |
| 319 | + .Select((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) | |
| 320 | + .ToListAsync(); | |
| 321 | + | |
| 322 | + var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); | |
| 323 | + | |
| 324 | + var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 325 | + .Where((t, l, p, lc, pc, loc) => | |
| 326 | + !string.IsNullOrEmpty(p.Id) && | |
| 327 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 328 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 329 | + .GroupBy((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, Cat = pc.CategoryName }) | |
| 330 | + .Select((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, CategoryName = pc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) | |
| 331 | + .ToListAsync(); | |
| 332 | + | |
| 333 | + var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); | |
| 334 | + var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList(); | |
| 335 | + | |
| 336 | + var trendEndDay = curEndExcl.Date.AddDays(-1); | |
| 337 | + var trendStartDay = trendEndDay.AddDays(-6); | |
| 338 | + if (trendStartDay < curStart.Date) | |
| 339 | + { | |
| 340 | + trendStartDay = curStart.Date; | |
| 341 | + } | |
| 342 | + | |
| 343 | + var trendEndExcl = trendEndDay.AddDays(1); | |
| 344 | + | |
| 345 | + var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 346 | + .Where((t, l, p, lc, pc, loc) => | |
| 347 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && | |
| 348 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) | |
| 349 | + .Select((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime)) | |
| 350 | + .ToListAsync(); | |
| 351 | + | |
| 352 | + var trendDict = trendRaw | |
| 353 | + .Where(x => x.HasValue) | |
| 354 | + .GroupBy(x => x!.Value.Date) | |
| 355 | + .ToDictionary(g => g.Key, g => g.Count()); | |
| 356 | + | |
| 357 | + var trend = new List<ReportsDailyCountDto>(); | |
| 358 | + for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1)) | |
| 359 | + { | |
| 360 | + trend.Add(new ReportsDailyCountDto | |
| 361 | + { | |
| 362 | + Date = d.ToString("yyyy-MM-dd"), | |
| 363 | + Count = trendDict.TryGetValue(d, out var c) ? c : 0 | |
| 364 | + }); | |
| 365 | + } | |
| 366 | + | |
| 367 | + var byCategory = categoryRows | |
| 368 | + .OrderByDescending(x => x.Cnt) | |
| 369 | + .Select(x => new ReportsCategoryCountDto | |
| 370 | + { | |
| 371 | + CategoryId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), | |
| 372 | + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName.Trim(), | |
| 373 | + Count = x.Cnt | |
| 374 | + }) | |
| 375 | + .ToList(); | |
| 376 | + | |
| 377 | + var mostUsed = topList.Select(x => | |
| 378 | + { | |
| 379 | + var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2); | |
| 380 | + return new ReportsTopProductRowDto | |
| 381 | + { | |
| 382 | + ProductId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), | |
| 383 | + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? null : x.ProductName.Trim(), | |
| 384 | + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName!.Trim(), | |
| 385 | + TotalPrinted = x.Cnt, | |
| 386 | + UsagePercent = pct | |
| 387 | + }; | |
| 388 | + }).ToList(); | |
| 389 | + | |
| 390 | + return new ReportsLabelReportOutputDto | |
| 391 | + { | |
| 392 | + Summary = new ReportsLabelReportSummaryDto | |
| 393 | + { | |
| 394 | + TotalLabelsPrinted = totalCur, | |
| 395 | + TotalLabelsPrintedPrevPeriod = totalPrev, | |
| 396 | + TotalLabelsPrintedChangeRate = CalcChangeRate(totalCur, totalPrev), | |
| 397 | + MostPrintedCategoryName = string.IsNullOrWhiteSpace(topCat?.CategoryName) ? null : topCat.CategoryName.Trim(), | |
| 398 | + MostPrintedCategoryCount = topCat?.Cnt ?? 0, | |
| 399 | + TopProductName = string.IsNullOrWhiteSpace(topProd?.ProductName) ? null : topProd.ProductName.Trim(), | |
| 400 | + TopProductCount = topProd?.Cnt ?? 0, | |
| 401 | + AvgDailyPrints = avgDaily, | |
| 402 | + AvgDailyPrintsPrevPeriod = avgDailyPrev, | |
| 403 | + AvgDailyPrintsChangeRate = CalcChangeRate(avgDaily, avgDailyPrev) | |
| 404 | + }, | |
| 405 | + LabelsByCategory = byCategory, | |
| 406 | + PrintVolumeTrend = trend, | |
| 407 | + MostUsedProducts = mostUsed | |
| 408 | + }; | |
| 409 | + } | |
| 410 | + | |
| 411 | + /// <inheritdoc /> | |
| 412 | + public async Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input) | |
| 413 | + { | |
| 414 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 415 | + var data = await GetLabelReportAsync(input); | |
| 416 | + var fileName = $"label-report_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 417 | + var document = Document.Create(container => | |
| 418 | + { | |
| 419 | + container.Page(page => | |
| 420 | + { | |
| 421 | + page.Margin(24); | |
| 422 | + page.DefaultTextStyle(x => x.FontSize(9)); | |
| 423 | + page.Header().Text("Label Report").SemiBold().FontSize(16); | |
| 424 | + page.Content().Column(col => | |
| 425 | + { | |
| 426 | + col.Spacing(10); | |
| 427 | + col.Item().Text( | |
| 428 | + $"Total printed: {data.Summary.TotalLabelsPrinted} (prev: {data.Summary.TotalLabelsPrintedPrevPeriod}, Δ%: {data.Summary.TotalLabelsPrintedChangeRate:0.##}%)"); | |
| 429 | + col.Item().Text( | |
| 430 | + $"Top category: {data.Summary.MostPrintedCategoryName} ({data.Summary.MostPrintedCategoryCount})"); | |
| 431 | + col.Item().Text($"Top product: {data.Summary.TopProductName} ({data.Summary.TopProductCount})"); | |
| 432 | + col.Item().Text( | |
| 433 | + $"Avg daily: {data.Summary.AvgDailyPrints:0.##} (prev: {data.Summary.AvgDailyPrintsPrevPeriod:0.##}, Δ%: {data.Summary.AvgDailyPrintsChangeRate:0.##}%)"); | |
| 434 | + col.Item().Text("By category:").SemiBold(); | |
| 435 | + col.Item().Table(t => | |
| 436 | + { | |
| 437 | + t.ColumnsDefinition(c => { c.RelativeColumn(2); c.RelativeColumn(1); }); | |
| 438 | + t.Cell().Element(HeaderCell).Text("Category"); | |
| 439 | + t.Cell().Element(HeaderCell).Text("Count"); | |
| 440 | + foreach (var r in data.LabelsByCategory) | |
| 441 | + { | |
| 442 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); | |
| 443 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); | |
| 444 | + } | |
| 445 | + }); | |
| 446 | + col.Item().Text("Daily trend:").SemiBold(); | |
| 447 | + col.Item().Table(t => | |
| 448 | + { | |
| 449 | + t.ColumnsDefinition(c => { c.RelativeColumn(1.2f); c.RelativeColumn(1); }); | |
| 450 | + t.Cell().Element(HeaderCell).Text("Date"); | |
| 451 | + t.Cell().Element(HeaderCell).Text("Count"); | |
| 452 | + foreach (var r in data.PrintVolumeTrend) | |
| 453 | + { | |
| 454 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Date); | |
| 455 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); | |
| 456 | + } | |
| 457 | + }); | |
| 458 | + col.Item().Text("Most used products:").SemiBold(); | |
| 459 | + col.Item().Table(t => | |
| 460 | + { | |
| 461 | + t.ColumnsDefinition(c => | |
| 462 | + { | |
| 463 | + c.RelativeColumn(1.5f); | |
| 464 | + c.RelativeColumn(1f); | |
| 465 | + c.RelativeColumn(0.8f); | |
| 466 | + c.RelativeColumn(0.7f); | |
| 467 | + }); | |
| 468 | + t.Cell().Element(HeaderCell).Text("Product"); | |
| 469 | + t.Cell().Element(HeaderCell).Text("Category"); | |
| 470 | + t.Cell().Element(HeaderCell).Text("Total"); | |
| 471 | + t.Cell().Element(HeaderCell).Text("%"); | |
| 472 | + foreach (var r in data.MostUsedProducts) | |
| 473 | + { | |
| 474 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.ProductName); | |
| 475 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); | |
| 476 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.TotalPrinted.ToString()); | |
| 477 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.UsagePercent.ToString("0.##")); | |
| 478 | + } | |
| 479 | + }); | |
| 480 | + }); | |
| 481 | + }); | |
| 482 | + }); | |
| 483 | + | |
| 484 | + static IContainer HeaderCell(IContainer x) => | |
| 485 | + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); | |
| 486 | + | |
| 487 | + var ms = new MemoryStream(); | |
| 488 | + document.GeneratePdf(ms); | |
| 489 | + ms.Position = 0; | |
| 490 | + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; | |
| 491 | + } | |
| 492 | + | |
| 493 | + private ISugarQueryable<FlLabelPrintTaskDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, | |
| 494 | + FlProductCategoryDbEntity, LocationAggregateRoot> | |
| 495 | + BuildReportTaskCore( | |
| 496 | + List<string>? locationIds, | |
| 497 | + bool isAdmin, | |
| 498 | + string currentUserIdStr, | |
| 499 | + string? keyword) | |
| 500 | + { | |
| 501 | + return _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 502 | + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) | |
| 503 | + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id) | |
| 504 | + .LeftJoin<FlLabelCategoryDbEntity>((t, l, p, lc) => l.LabelCategoryId == lc.Id) | |
| 505 | + .LeftJoin<FlProductCategoryDbEntity>((t, l, p, lc, pc) => p.CategoryId == pc.Id) | |
| 506 | + .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) => | |
| 507 | + t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) | |
| 508 | + .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) | |
| 509 | + .WhereIF(!isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) | |
| 510 | + .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) | |
| 511 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), | |
| 512 | + (t, l, p, lc, pc, loc) => | |
| 513 | + (p.ProductName != null && p.ProductName.Contains(keyword!)) || | |
| 514 | + (lc.CategoryName != null && lc.CategoryName.Contains(keyword!)) || | |
| 515 | + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!))); | |
| 516 | + } | |
| 517 | + | |
| 518 | + private static decimal CalcChangeRate(decimal current, decimal previous) | |
| 519 | + { | |
| 520 | + if (previous == 0) | |
| 521 | + { | |
| 522 | + return current > 0 ? 100m : 0m; | |
| 523 | + } | |
| 524 | + | |
| 525 | + return Math.Round((current - previous) * 100m / previous, 2); | |
| 526 | + } | |
| 527 | + | |
| 528 | + private static decimal CalcChangeRate(int current, int previous) => | |
| 529 | + CalcChangeRate((decimal)current, (decimal)previous); | |
| 530 | + | |
| 531 | + private async Task<List<string>?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, | |
| 532 | + string? locationId) | |
| 533 | + { | |
| 534 | + var locId = locationId?.Trim(); | |
| 535 | + if (!string.IsNullOrWhiteSpace(locId)) | |
| 536 | + { | |
| 537 | + return new List<string> { locId }; | |
| 538 | + } | |
| 539 | + | |
| 540 | + var gid = groupId?.Trim(); | |
| 541 | + var pid = partnerId?.Trim(); | |
| 542 | + | |
| 543 | + if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) | |
| 544 | + { | |
| 545 | + return null; | |
| 546 | + } | |
| 547 | + | |
| 548 | + var q = _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>().Where(x => !x.IsDeleted); | |
| 549 | + | |
| 550 | + if (!string.IsNullOrWhiteSpace(gid)) | |
| 551 | + { | |
| 552 | + var g = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 553 | + .FirstAsync(x => !x.IsDeleted && x.Id == gid); | |
| 554 | + if (g is null) | |
| 555 | + { | |
| 556 | + return new List<string>(); | |
| 557 | + } | |
| 558 | + | |
| 559 | + var gName = g.GroupName?.Trim() ?? string.Empty; | |
| 560 | + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 561 | + .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); | |
| 562 | + var pName = partner?.PartnerName?.Trim() ?? string.Empty; | |
| 563 | + q = q.Where(x => x.GroupName == gName && x.Partner == pName); | |
| 564 | + } | |
| 565 | + else if (!string.IsNullOrWhiteSpace(pid)) | |
| 566 | + { | |
| 567 | + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 568 | + .FirstAsync(x => !x.IsDeleted && x.Id == pid); | |
| 569 | + if (partner is null) | |
| 570 | + { | |
| 571 | + return new List<string>(); | |
| 572 | + } | |
| 573 | + | |
| 574 | + var pName = partner.PartnerName?.Trim() ?? string.Empty; | |
| 575 | + q = q.Where(x => x.Partner == pName); | |
| 576 | + } | |
| 577 | + | |
| 578 | + var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); | |
| 579 | + return ids; | |
| 580 | + } | |
| 581 | + | |
| 582 | + private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, | |
| 583 | + DateTime? endDate) | |
| 584 | + { | |
| 585 | + var endDay = (endDate ?? DateTime.Today).Date; | |
| 586 | + var endExcl = endDay.AddDays(1); | |
| 587 | + var start = (startDate ?? endDay.AddDays(-29)).Date; | |
| 588 | + if (start >= endExcl) | |
| 589 | + { | |
| 590 | + start = endExcl.AddDays(-1); | |
| 591 | + } | |
| 592 | + | |
| 593 | + return (start, endExcl); | |
| 594 | + } | |
| 595 | + | |
| 596 | + private static PagedResultWithPageDto<ReportsPrintLogListItemDto> EmptyPrintLogPage( | |
| 597 | + ReportsPrintLogGetListInputVo input) | |
| 598 | + { | |
| 599 | + var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; | |
| 600 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); | |
| 601 | + return new PagedResultWithPageDto<ReportsPrintLogListItemDto> | |
| 602 | + { | |
| 603 | + PageIndex = pageIndex, | |
| 604 | + PageSize = pageSize, | |
| 605 | + TotalCount = 0, | |
| 606 | + TotalPages = 0, | |
| 607 | + Items = new List<ReportsPrintLogListItemDto>() | |
| 608 | + }; | |
| 609 | + } | |
| 610 | + | |
| 611 | + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, | |
| 612 | + List<T> items) | |
| 613 | + { | |
| 614 | + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | |
| 615 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); | |
| 616 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | |
| 617 | + return new PagedResultWithPageDto<T> | |
| 618 | + { | |
| 619 | + PageIndex = pageIndex, | |
| 620 | + PageSize = pageSize, | |
| 621 | + TotalCount = total, | |
| 622 | + TotalPages = totalPages, | |
| 623 | + Items = items | |
| 624 | + }; | |
| 625 | + } | |
| 626 | + | |
| 627 | + private async Task<Dictionary<string, string>> LoadUserNameMapAsync(List<string> userIdStrings) | |
| 628 | + { | |
| 629 | + var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| 630 | + if (userIdStrings.Count == 0) | |
| 631 | + { | |
| 632 | + return map; | |
| 633 | + } | |
| 634 | + | |
| 635 | + var guids = userIdStrings | |
| 636 | + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) | |
| 637 | + .Where(x => x.HasValue) | |
| 638 | + .Select(x => x!.Value) | |
| 639 | + .Distinct() | |
| 640 | + .ToList(); | |
| 641 | + if (guids.Count == 0) | |
| 642 | + { | |
| 643 | + return map; | |
| 644 | + } | |
| 645 | + | |
| 646 | + var users = await _dbContext.SqlSugarClient.Queryable<UserAggregateRoot>() | |
| 647 | + .Where(u => !u.IsDeleted && guids.Contains(u.Id)) | |
| 648 | + .Select(u => new { u.Id, u.Name, u.UserName }) | |
| 649 | + .ToListAsync(); | |
| 650 | + | |
| 651 | + foreach (var u in users) | |
| 652 | + { | |
| 653 | + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); | |
| 654 | + map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; | |
| 655 | + } | |
| 656 | + | |
| 657 | + return map; | |
| 658 | + } | |
| 659 | + | |
| 660 | + private static string ResolveUserName(Dictionary<string, string> map, string? createdBy) | |
| 661 | + { | |
| 662 | + if (string.IsNullOrWhiteSpace(createdBy)) | |
| 663 | + { | |
| 664 | + return "无"; | |
| 665 | + } | |
| 666 | + | |
| 667 | + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; | |
| 668 | + } | |
| 669 | + | |
| 670 | + private static string FormatLocationText(string? locName, string? locCode) | |
| 671 | + { | |
| 672 | + var n = locName?.Trim(); | |
| 673 | + var c = locCode?.Trim(); | |
| 674 | + if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(c)) | |
| 675 | + { | |
| 676 | + return "无"; | |
| 677 | + } | |
| 678 | + | |
| 679 | + if (string.IsNullOrWhiteSpace(c)) | |
| 680 | + { | |
| 681 | + return n ?? "无"; | |
| 682 | + } | |
| 683 | + | |
| 684 | + if (string.IsNullOrWhiteSpace(n)) | |
| 685 | + { | |
| 686 | + return $"({c})"; | |
| 687 | + } | |
| 688 | + | |
| 689 | + return $"{n} ({c})"; | |
| 690 | + } | |
| 691 | + | |
| 692 | + private static string FormatTemplateDisplay(decimal w, decimal h, string? unit, string? templateName) | |
| 693 | + { | |
| 694 | + var size = FormatLabelSizeWithUnit(w, h, unit ?? "inch"); | |
| 695 | + var tn = templateName?.Trim(); | |
| 696 | + if (string.IsNullOrWhiteSpace(tn)) | |
| 697 | + { | |
| 698 | + return size ?? "无"; | |
| 699 | + } | |
| 700 | + | |
| 701 | + return string.IsNullOrWhiteSpace(size) ? tn : $"{size} {tn}"; | |
| 702 | + } | |
| 703 | + | |
| 704 | + private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit) | |
| 705 | + { | |
| 706 | + var u = (unit ?? "inch").Trim().ToLowerInvariant(); | |
| 707 | + var ws = w.ToString(CultureInfo.InvariantCulture); | |
| 708 | + var hs = h.ToString(CultureInfo.InvariantCulture); | |
| 709 | + var normalizedUnit = u is "in" ? "inch" : u; | |
| 710 | + return $"{ws}x{hs}{normalizedUnit}"; | |
| 711 | + } | |
| 712 | + | |
| 713 | + private static string TryExtractExpiryText(string? printInputJson) | |
| 714 | + { | |
| 715 | + if (string.IsNullOrWhiteSpace(printInputJson)) | |
| 716 | + { | |
| 717 | + return "无"; | |
| 718 | + } | |
| 719 | + | |
| 720 | + try | |
| 721 | + { | |
| 722 | + using var doc = JsonDocument.Parse(printInputJson); | |
| 723 | + if (doc.RootElement.ValueKind != JsonValueKind.Object) | |
| 724 | + { | |
| 725 | + return "无"; | |
| 726 | + } | |
| 727 | + | |
| 728 | + foreach (var prop in doc.RootElement.EnumerateObject()) | |
| 729 | + { | |
| 730 | + var key = prop.Name.Trim(); | |
| 731 | + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && | |
| 732 | + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && | |
| 733 | + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) | |
| 734 | + { | |
| 735 | + continue; | |
| 736 | + } | |
| 737 | + | |
| 738 | + var v = prop.Value; | |
| 739 | + if (v.ValueKind == JsonValueKind.String) | |
| 740 | + { | |
| 741 | + var s = v.GetString(); | |
| 742 | + return string.IsNullOrWhiteSpace(s) ? "无" : s.Trim(); | |
| 743 | + } | |
| 744 | + | |
| 745 | + if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n)) | |
| 746 | + { | |
| 747 | + return n.ToString(CultureInfo.InvariantCulture); | |
| 748 | + } | |
| 749 | + | |
| 750 | + return v.ToString(); | |
| 751 | + } | |
| 752 | + } | |
| 753 | + catch | |
| 754 | + { | |
| 755 | + return "无"; | |
| 756 | + } | |
| 757 | + | |
| 758 | + return "无"; | |
| 759 | + } | |
| 760 | + | |
| 761 | + private static IActionResult BuildEmptyPdf(string fileName) | |
| 762 | + { | |
| 763 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 764 | + var document = Document.Create(c => | |
| 765 | + { | |
| 766 | + c.Page(p => | |
| 767 | + { | |
| 768 | + p.Margin(30); | |
| 769 | + p.Content().Text("No data for current filters."); | |
| 770 | + }); | |
| 771 | + }); | |
| 772 | + var ms = new MemoryStream(); | |
| 773 | + document.GeneratePdf(ms); | |
| 774 | + ms.Position = 0; | |
| 775 | + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; | |
| 776 | + } | |
| 777 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
| 1 | 1 | using System; |
| 2 | 2 | using System.Collections.Generic; |
| 3 | +using System.Globalization; | |
| 3 | 4 | using System.IdentityModel.Tokens.Jwt; |
| 4 | 5 | using System.Linq; |
| 5 | 6 | using System.Security.Claims; |
| 6 | 7 | using System.Text; |
| 7 | 8 | using System.Threading.Tasks; |
| 9 | +using FoodLabeling.Application.Contracts; | |
| 8 | 10 | using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; |
| 9 | 11 | using FoodLabeling.Application.Contracts.IServices; |
| 10 | 12 | using FoodLabeling.Application.Services.DbModels; |
| ... | ... | @@ -20,6 +22,7 @@ using Volo.Abp; |
| 20 | 22 | using Volo.Abp.Application.Services; |
| 21 | 23 | using Volo.Abp.EventBus.Local; |
| 22 | 24 | using Volo.Abp.Security.Claims; |
| 25 | +using Volo.Abp.Uow; | |
| 23 | 26 | using Volo.Abp.Users; |
| 24 | 27 | using Yi.Framework.Core.Helper; |
| 25 | 28 | using Yi.Framework.Rbac.Domain.Entities; |
| ... | ... | @@ -135,6 +138,267 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 135 | 138 | return await LoadBoundLocationsAsync(CurrentUser.Id.Value); |
| 136 | 139 | } |
| 137 | 140 | |
| 141 | + /// <summary> | |
| 142 | + /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户) | |
| 143 | + /// </summary> | |
| 144 | + /// <remarks> | |
| 145 | + /// 仅当当前登录用户在 <c>userlocation</c> 中绑定该 <c>locationId</c> 时可查;否则返回业务异常。 | |
| 146 | + /// | |
| 147 | + /// 店长:在同店绑定用户中,取 <c>Role.RoleCode</c> 或 <c>Role.RoleName</c>(忽略大小写)包含 <c>manager</c> 的第一条; | |
| 148 | + /// 若无匹配则店长姓名与电话均为「无」。 | |
| 149 | + /// | |
| 150 | + /// <c>OperatingHours</c>:当前 <c>location</c> 表无营业时间字段,固定返回「无」。 | |
| 151 | + /// </remarks> | |
| 152 | + /// <param name="locationId">门店主键(Guid 字符串)</param> | |
| 153 | + /// <returns>与原型一致的展示字段</returns> | |
| 154 | + /// <response code="200">成功</response> | |
| 155 | + /// <response code="400">未登录、门店标识无效、未绑定或门店不存在</response> | |
| 156 | + /// <response code="500">服务器错误</response> | |
| 157 | + [Authorize] | |
| 158 | + public virtual async Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId) | |
| 159 | + { | |
| 160 | + if (!CurrentUser.Id.HasValue) | |
| 161 | + { | |
| 162 | + throw new UserFriendlyException("用户未登录"); | |
| 163 | + } | |
| 164 | + | |
| 165 | + var lid = (locationId ?? string.Empty).Trim(); | |
| 166 | + if (string.IsNullOrEmpty(lid) || !Guid.TryParse(lid, out var locationGuid)) | |
| 167 | + { | |
| 168 | + throw new UserFriendlyException("无效的门店标识"); | |
| 169 | + } | |
| 170 | + | |
| 171 | + var userIdStr = CurrentUser.Id.Value.ToString(); | |
| 172 | + var bound = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 173 | + .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid); | |
| 174 | + if (!bound) | |
| 175 | + { | |
| 176 | + throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); | |
| 177 | + } | |
| 178 | + | |
| 179 | + var locRows = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | |
| 180 | + .Where(x => !x.IsDeleted && x.Id == locationGuid) | |
| 181 | + .ToListAsync(); | |
| 182 | + var loc = locRows.FirstOrDefault(); | |
| 183 | + if (loc is null) | |
| 184 | + { | |
| 185 | + throw new UserFriendlyException("门店不存在或已删除"); | |
| 186 | + } | |
| 187 | + | |
| 188 | + var (mgrName, mgrPhone) = await TryResolveStoreManagerAsync(lid); | |
| 189 | + | |
| 190 | + return new UsAppLocationDetailOutputDto | |
| 191 | + { | |
| 192 | + LocationId = loc.Id.ToString(), | |
| 193 | + LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(), | |
| 194 | + FullAddress = BuildFullAddress(loc), | |
| 195 | + StorePhone = FormatStorePhoneDisplay(loc.Phone), | |
| 196 | + OperatingHours = "无", | |
| 197 | + ManagerName = mgrName, | |
| 198 | + ManagerPhone = mgrPhone | |
| 199 | + }; | |
| 200 | + } | |
| 201 | + | |
| 202 | + /// <inheritdoc /> | |
| 203 | + [Authorize] | |
| 204 | + public virtual async Task<UsAppMyProfileOutputDto> GetMyProfileAsync() | |
| 205 | + { | |
| 206 | + if (!CurrentUser.Id.HasValue) | |
| 207 | + { | |
| 208 | + throw new UserFriendlyException("用户未登录"); | |
| 209 | + } | |
| 210 | + | |
| 211 | + var userId = CurrentUser.Id.Value; | |
| 212 | + var user = await _userRepository.GetByIdAsync(userId); | |
| 213 | + if (user is null || user.IsDeleted || !user.State) | |
| 214 | + { | |
| 215 | + throw new UserFriendlyException("用户不存在或已停用"); | |
| 216 | + } | |
| 217 | + | |
| 218 | + // 避免 SqlSugar 在该环境下对 Role 关联表达式解析异常(Select 不支持),这里改用显式 SQL 查询角色。 | |
| 219 | + var roleRows = await _dbContext.SqlSugarClient.Ado.SqlQueryAsync<MyProfileRoleRow>( | |
| 220 | + @"SELECT r.RoleName, r.RoleCode | |
| 221 | + FROM UserRole ur | |
| 222 | + INNER JOIN Role r ON ur.RoleId = r.Id | |
| 223 | + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 | |
| 224 | + ORDER BY r.OrderNum ASC", | |
| 225 | + new { UserId = userId }); | |
| 226 | + | |
| 227 | + var roleNames = roleRows.Select(x => x.RoleName?.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); | |
| 228 | + var roleDisplay = roleNames.Count == 0 ? "无" : string.Join(", ", roleNames); | |
| 229 | + var primaryCode = roleRows.FirstOrDefault()?.RoleCode?.Trim(); | |
| 230 | + | |
| 231 | + var fullName = !string.IsNullOrWhiteSpace(user.Name?.Trim()) | |
| 232 | + ? user.Name.Trim() | |
| 233 | + : (!string.IsNullOrWhiteSpace(user.Nick?.Trim()) ? user.Nick.Trim() : user.UserName.Trim()); | |
| 234 | + | |
| 235 | + return new UsAppMyProfileOutputDto | |
| 236 | + { | |
| 237 | + FullName = fullName, | |
| 238 | + Email = string.IsNullOrWhiteSpace(user.Email) ? "无" : user.Email.Trim(), | |
| 239 | + Phone = FormatPhoneDisplay(user.Phone), | |
| 240 | + EmployeeId = string.IsNullOrWhiteSpace(user.UserName) ? "无" : user.UserName.Trim(), | |
| 241 | + RoleDisplay = roleDisplay, | |
| 242 | + PrimaryRoleCode = string.IsNullOrWhiteSpace(primaryCode) ? null : primaryCode | |
| 243 | + }; | |
| 244 | + } | |
| 245 | + | |
| 246 | + /// <inheritdoc /> | |
| 247 | + [Authorize] | |
| 248 | + [UnitOfWork] | |
| 249 | + public virtual async Task ChangePasswordAsync(UsAppChangePasswordInputVo input) | |
| 250 | + { | |
| 251 | + if (input is null) | |
| 252 | + { | |
| 253 | + throw new UserFriendlyException("入参不能为空"); | |
| 254 | + } | |
| 255 | + | |
| 256 | + if (!CurrentUser.Id.HasValue) | |
| 257 | + { | |
| 258 | + throw new UserFriendlyException("用户未登录"); | |
| 259 | + } | |
| 260 | + | |
| 261 | + var current = input.CurrentPassword ?? string.Empty; | |
| 262 | + var newPwd = input.NewPassword ?? string.Empty; | |
| 263 | + var confirm = input.ConfirmNewPassword ?? string.Empty; | |
| 264 | + | |
| 265 | + if (string.IsNullOrWhiteSpace(current) || string.IsNullOrWhiteSpace(newPwd) || string.IsNullOrWhiteSpace(confirm)) | |
| 266 | + { | |
| 267 | + throw new UserFriendlyException("请填写当前密码、新密码与确认密码"); | |
| 268 | + } | |
| 269 | + | |
| 270 | + if (!string.Equals(newPwd, confirm, StringComparison.Ordinal)) | |
| 271 | + { | |
| 272 | + throw new UserFriendlyException("新密码与确认密码不一致"); | |
| 273 | + } | |
| 274 | + | |
| 275 | + if (string.Equals(current, newPwd, StringComparison.Ordinal)) | |
| 276 | + { | |
| 277 | + throw new UserFriendlyException("新密码不能与当前密码相同"); | |
| 278 | + } | |
| 279 | + | |
| 280 | + ValidateAppPasswordComplexity(newPwd); | |
| 281 | + | |
| 282 | + var userId = CurrentUser.Id.Value; | |
| 283 | + var user = await _userRepository.GetByIdAsync(userId); | |
| 284 | + if (user is null || user.IsDeleted || !user.State) | |
| 285 | + { | |
| 286 | + throw new UserFriendlyException("用户不存在或已停用"); | |
| 287 | + } | |
| 288 | + | |
| 289 | + if (!user.JudgePassword(current)) | |
| 290 | + { | |
| 291 | + throw new UserFriendlyException(UserConst.Login_Error); | |
| 292 | + } | |
| 293 | + | |
| 294 | + user.EncryPassword.Password = newPwd; | |
| 295 | + user.BuildPassword(); | |
| 296 | + await _userRepository.UpdateAsync(user); | |
| 297 | + } | |
| 298 | + | |
| 299 | + private static string FormatPhoneDisplay(long? phone) | |
| 300 | + { | |
| 301 | + if (!phone.HasValue) | |
| 302 | + { | |
| 303 | + return "无"; | |
| 304 | + } | |
| 305 | + | |
| 306 | + var digits = Math.Abs(phone.Value).ToString(CultureInfo.InvariantCulture); | |
| 307 | + if (digits.Length == 10) | |
| 308 | + { | |
| 309 | + return $"+1 ({digits[..3]}) {digits.Substring(3, 3)}-{digits.Substring(6, 4)}"; | |
| 310 | + } | |
| 311 | + | |
| 312 | + if (digits.Length == 11 && digits.StartsWith("1", StringComparison.Ordinal)) | |
| 313 | + { | |
| 314 | + return $"+1 ({digits[1..4]}) {digits.Substring(4, 3)}-{digits.Substring(7, 4)}"; | |
| 315 | + } | |
| 316 | + | |
| 317 | + return $"+{digits}"; | |
| 318 | + } | |
| 319 | + | |
| 320 | + private static string FormatStorePhoneDisplay(string? phone) | |
| 321 | + { | |
| 322 | + var t = phone?.Trim(); | |
| 323 | + return string.IsNullOrEmpty(t) ? "无" : t; | |
| 324 | + } | |
| 325 | + | |
| 326 | + private async Task<(string Name, string Phone)> TryResolveStoreManagerAsync(string locationIdTrimmed) | |
| 327 | + { | |
| 328 | + var userIdStrings = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 329 | + .Where(x => !x.IsDeleted && x.LocationId == locationIdTrimmed) | |
| 330 | + .Select(x => x.UserId) | |
| 331 | + .Distinct() | |
| 332 | + .ToListAsync(); | |
| 333 | + | |
| 334 | + var userGuids = userIdStrings | |
| 335 | + .Select(s => Guid.TryParse(s, out var g) ? (Guid?)g : null) | |
| 336 | + .Where(g => g.HasValue) | |
| 337 | + .Select(g => g!.Value) | |
| 338 | + .ToList(); | |
| 339 | + | |
| 340 | + if (userGuids.Count == 0) | |
| 341 | + { | |
| 342 | + return ("无", "无"); | |
| 343 | + } | |
| 344 | + | |
| 345 | + var rows = await _dbContext.SqlSugarClient.Ado.SqlQueryAsync<LocationManagerRow>( | |
| 346 | + @"SELECT u.Name, u.Nick, u.UserName, u.Phone | |
| 347 | + FROM User u | |
| 348 | + INNER JOIN UserRole ur ON u.Id = ur.UserId | |
| 349 | + INNER JOIN Role r ON ur.RoleId = r.Id | |
| 350 | + WHERE u.IsDeleted = 0 | |
| 351 | + AND u.State = 1 | |
| 352 | + AND r.IsDeleted = 0 | |
| 353 | + AND r.State = 1 | |
| 354 | + AND (LOWER(r.RoleCode) LIKE '%manager%' OR LOWER(r.RoleName) LIKE '%manager%') | |
| 355 | + AND u.Id IN (@UserIds) | |
| 356 | + ORDER BY u.Name ASC", | |
| 357 | + new { UserIds = userGuids }); | |
| 358 | + | |
| 359 | + var row = rows.FirstOrDefault(); | |
| 360 | + if (row is null) | |
| 361 | + { | |
| 362 | + return ("无", "无"); | |
| 363 | + } | |
| 364 | + | |
| 365 | + var displayName = !string.IsNullOrWhiteSpace(row.Name?.Trim()) | |
| 366 | + ? row.Name!.Trim() | |
| 367 | + : (!string.IsNullOrWhiteSpace(row.Nick?.Trim()) | |
| 368 | + ? row.Nick!.Trim() | |
| 369 | + : (row.UserName?.Trim() ?? "无")); | |
| 370 | + | |
| 371 | + return (displayName, FormatPhoneDisplay(row.Phone)); | |
| 372 | + } | |
| 373 | + | |
| 374 | + private static void ValidateAppPasswordComplexity(string password) | |
| 375 | + { | |
| 376 | + if (password.Length < 8) | |
| 377 | + { | |
| 378 | + throw new UserFriendlyException("新密码至少 8 位"); | |
| 379 | + } | |
| 380 | + | |
| 381 | + if (!password.Any(char.IsUpper)) | |
| 382 | + { | |
| 383 | + throw new UserFriendlyException("新密码需包含大写字母"); | |
| 384 | + } | |
| 385 | + | |
| 386 | + if (!password.Any(char.IsLower)) | |
| 387 | + { | |
| 388 | + throw new UserFriendlyException("新密码需包含小写字母"); | |
| 389 | + } | |
| 390 | + | |
| 391 | + if (!password.Any(char.IsDigit)) | |
| 392 | + { | |
| 393 | + throw new UserFriendlyException("新密码需包含至少一个数字"); | |
| 394 | + } | |
| 395 | + | |
| 396 | + if (!password.Any(c => !char.IsLetterOrDigit(c))) | |
| 397 | + { | |
| 398 | + throw new UserFriendlyException("新密码需包含至少一个特殊字符"); | |
| 399 | + } | |
| 400 | + } | |
| 401 | + | |
| 138 | 402 | private void ValidationImageCaptcha(string? uuid, string? code) |
| 139 | 403 | { |
| 140 | 404 | if (!_rbacOptions.EnableCaptcha) |
| ... | ... | @@ -149,16 +413,21 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 149 | 413 | } |
| 150 | 414 | |
| 151 | 415 | /// <summary> |
| 152 | - /// 按邮箱查找未删除且启用的用户(邮箱比较忽略大小写) | |
| 416 | + /// 按邮箱或用户名(邮箱形字符串写在 UserName 时)查找未删除且启用的用户;比较忽略大小写,Email 命中优先。 | |
| 153 | 417 | /// </summary> |
| 154 | 418 | private async Task<UserAggregateRoot?> FindActiveUserByEmailAsync(string email) |
| 155 | 419 | { |
| 156 | 420 | var normalized = email.Trim().ToLowerInvariant(); |
| 157 | 421 | var users = await _userRepository._DbQueryable |
| 158 | 422 | .Where(u => !u.IsDeleted && u.State == true) |
| 159 | - .Where(u => u.Email != null && SqlFunc.ToLower(u.Email) == normalized) | |
| 423 | + .Where(u => | |
| 424 | + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) || | |
| 425 | + SqlFunc.ToLower(u.UserName) == normalized) | |
| 160 | 426 | .ToListAsync(); |
| 161 | - return users.FirstOrDefault(); | |
| 427 | + return users.FirstOrDefault(u => | |
| 428 | + u.Email != null && | |
| 429 | + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase)) | |
| 430 | + ?? users.FirstOrDefault(); | |
| 162 | 431 | } |
| 163 | 432 | |
| 164 | 433 | private string CreateAppAccessToken(UserAggregateRoot user) |
| ... | ... | @@ -169,7 +438,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 169 | 438 | var claims = new List<Claim> |
| 170 | 439 | { |
| 171 | 440 | new(AbpClaimTypes.UserId, user.Id.ToString()), |
| 172 | - new(AbpClaimTypes.UserName, user.UserName) | |
| 441 | + new(AbpClaimTypes.UserName, user.UserName), | |
| 442 | + new(UsAppJwtClaims.ClientKind, UsAppJwtClaims.ClientKindUsApp) | |
| 173 | 443 | }; |
| 174 | 444 | |
| 175 | 445 | if (!string.IsNullOrWhiteSpace(user.Email)) |
| ... | ... | @@ -257,4 +527,22 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 257 | 527 | |
| 258 | 528 | return segments.Count == 0 ? "无" : string.Join(", ", segments); |
| 259 | 529 | } |
| 530 | + | |
| 531 | + private sealed class MyProfileRoleRow | |
| 532 | + { | |
| 533 | + public string? RoleName { get; set; } | |
| 534 | + | |
| 535 | + public string? RoleCode { get; set; } | |
| 536 | + } | |
| 537 | + | |
| 538 | + private sealed class LocationManagerRow | |
| 539 | + { | |
| 540 | + public string? Name { get; set; } | |
| 541 | + | |
| 542 | + public string? Nick { get; set; } | |
| 543 | + | |
| 544 | + public string? UserName { get; set; } | |
| 545 | + | |
| 546 | + public long? Phone { get; set; } | |
| 547 | + } | |
| 260 | 548 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
| ... | ... | @@ -50,8 +50,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 50 | 50 | /// 获取当前门店下四级嵌套数据 |
| 51 | 51 | /// </summary> |
| 52 | 52 | /// <remarks> |
| 53 | - /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category; | |
| 53 | + /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; | |
| 54 | + /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); | |
| 54 | 55 | /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 |
| 56 | + /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; | |
| 57 | + /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 | |
| 55 | 58 | /// </remarks> |
| 56 | 59 | [Authorize] |
| 57 | 60 | public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input) |
| ... | ... | @@ -83,10 +86,15 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 83 | 86 | LabelCategoryId = c.Id, |
| 84 | 87 | LabelCategoryName = c.CategoryName, |
| 85 | 88 | LabelCategoryPhotoUrl = c.CategoryPhotoUrl, |
| 89 | + LabelCategoryButtonAppearance = c.ButtonAppearance, | |
| 86 | 90 | LabelCategoryOrderNum = c.OrderNum, |
| 87 | 91 | ProductCategoryId = p.CategoryId, |
| 88 | 92 | ProductCategoryName = pc.CategoryName, |
| 89 | 93 | ProductCategoryPhotoUrl = pc.CategoryPhotoUrl, |
| 94 | + ProductCategoryDisplayText = pc.DisplayText, | |
| 95 | + ProductCategoryButtonAppearance = pc.ButtonAppearance, | |
| 96 | + ProductCategoryAvailabilityType = pc.AvailabilityType, | |
| 97 | + ProductCategoryOrderNum = pc.OrderNum, | |
| 90 | 98 | ProductId = p.Id, |
| 91 | 99 | ProductName = p.ProductName, |
| 92 | 100 | ProductCode = p.ProductCode, |
| ... | ... | @@ -112,17 +120,22 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 112 | 120 | x.LabelCategoryId, |
| 113 | 121 | x.LabelCategoryName, |
| 114 | 122 | x.LabelCategoryPhotoUrl, |
| 123 | + x.LabelCategoryButtonAppearance, | |
| 115 | 124 | x.LabelCategoryOrderNum |
| 116 | 125 | }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName); |
| 117 | 126 | |
| 118 | 127 | var result = new List<UsAppLabelCategoryTreeNodeDto>(); |
| 119 | 128 | foreach (var g1 in byL1) |
| 120 | 129 | { |
| 130 | + var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance) | |
| 131 | + ? "TEXT" | |
| 132 | + : g1.Key.LabelCategoryButtonAppearance.Trim(); | |
| 121 | 133 | var l1 = new UsAppLabelCategoryTreeNodeDto |
| 122 | 134 | { |
| 123 | 135 | Id = g1.Key.LabelCategoryId, |
| 124 | 136 | CategoryName = g1.Key.LabelCategoryName ?? string.Empty, |
| 125 | 137 | CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl, |
| 138 | + ButtonAppearance = l1Appearance, | |
| 126 | 139 | OrderNum = g1.Key.LabelCategoryOrderNum, |
| 127 | 140 | ProductCategories = new List<UsAppProductCategoryNodeDto>() |
| 128 | 141 | }; |
| ... | ... | @@ -136,7 +149,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 136 | 149 | { |
| 137 | 150 | CategoryId = (string?)null, |
| 138 | 151 | CategoryName = "无", |
| 139 | - CategoryPhotoUrl = (string?)null | |
| 152 | + CategoryPhotoUrl = (string?)null, | |
| 153 | + DisplayText = (string?)null, | |
| 154 | + ButtonAppearance = (string?)null, | |
| 155 | + AvailabilityType = (string?)null, | |
| 156 | + CategoryOrderNum = int.MaxValue | |
| 140 | 157 | }; |
| 141 | 158 | } |
| 142 | 159 | |
| ... | ... | @@ -146,19 +163,34 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 146 | 163 | { |
| 147 | 164 | CategoryId = (string?)categoryId, |
| 148 | 165 | CategoryName = categoryName, |
| 149 | - CategoryPhotoUrl = categoryPhotoUrl | |
| 166 | + CategoryPhotoUrl = categoryPhotoUrl, | |
| 167 | + DisplayText = NormalizeNullableUrl(x.ProductCategoryDisplayText), | |
| 168 | + ButtonAppearance = NormalizeNullableId(x.ProductCategoryButtonAppearance), | |
| 169 | + AvailabilityType = NormalizeNullableId(x.ProductCategoryAvailabilityType), | |
| 170 | + CategoryOrderNum = x.ProductCategoryOrderNum | |
| 150 | 171 | }; |
| 151 | 172 | }) |
| 152 | - .OrderBy(g => g.Key.CategoryName); | |
| 173 | + .OrderBy(g => g.Key.CategoryOrderNum) | |
| 174 | + .ThenBy(g => g.Key.CategoryName); | |
| 153 | 175 | |
| 154 | 176 | foreach (var g2 in byL2) |
| 155 | 177 | { |
| 156 | 178 | var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); |
| 179 | + var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance) | |
| 180 | + ? "TEXT" | |
| 181 | + : g2.Key.ButtonAppearance.Trim(); | |
| 182 | + var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType) | |
| 183 | + ? "ALL" | |
| 184 | + : g2.Key.AvailabilityType.Trim().ToUpperInvariant(); | |
| 157 | 185 | var l2 = new UsAppProductCategoryNodeDto |
| 158 | 186 | { |
| 159 | 187 | CategoryId = g2.Key.CategoryId, |
| 160 | 188 | CategoryPhotoUrl = g2.Key.CategoryPhotoUrl, |
| 161 | 189 | Name = g2.Key.CategoryName, |
| 190 | + DisplayText = g2.Key.DisplayText, | |
| 191 | + ButtonAppearance = appearance, | |
| 192 | + AvailabilityType = availability, | |
| 193 | + OrderNum = g2.Key.CategoryOrderNum == int.MaxValue ? 0 : g2.Key.CategoryOrderNum, | |
| 162 | 194 | ItemCount = productsGrouped.Count(), |
| 163 | 195 | Products = new List<UsAppLabelingProductNodeDto>() |
| 164 | 196 | }; |
| ... | ... | @@ -424,7 +456,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 424 | 456 | } |
| 425 | 457 | |
| 426 | 458 | var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId); |
| 427 | - var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value); | |
| 459 | + var normalizedPrintInput = ParsePrintInputJsonToDictionary(input.PrintInputJson); | |
| 428 | 460 | |
| 429 | 461 | // 解析模板 elements(与预览一致的渲染数据) |
| 430 | 462 | var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo |
| ... | ... | @@ -581,8 +613,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 581 | 613 | throw new UserFriendlyException("打印任务不存在"); |
| 582 | 614 | } |
| 583 | 615 | |
| 584 | - // 仅允许重打自己在当前门店的任务 | |
| 585 | - if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) | |
| 616 | + // 非 admin:仅允许重打自己在当前门店的任务;admin 可重打任意用户任务(仍须门店一致) | |
| 617 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 618 | + if (!isAdmin && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) | |
| 586 | 619 | { |
| 587 | 620 | throw new UserFriendlyException("无权限重打该任务"); |
| 588 | 621 | } |
| ... | ... | @@ -852,14 +885,29 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 852 | 885 | .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) |
| 853 | 886 | .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) |
| 854 | 887 | .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) |
| 855 | - .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) | |
| 888 | + .Where((lp, l, p, c, t, tpl, pc) => | |
| 889 | + !c.IsDeleted && c.State && | |
| 890 | + (c.AvailabilityType == "ALL" || | |
| 891 | + (c.AvailabilityType == "SPECIFIED" && | |
| 892 | + SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() | |
| 893 | + .Where(loc => loc.CategoryId == c.Id && loc.LocationId == locationId) | |
| 894 | + .Any()))) | |
| 856 | 895 | .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) |
| 857 | 896 | .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) |
| 897 | + .Where((lp, l, p, c, t, tpl, pc) => | |
| 898 | + pc.Id == null || | |
| 899 | + (!pc.IsDeleted && pc.State && | |
| 900 | + (pc.AvailabilityType == "ALL" || | |
| 901 | + (pc.AvailabilityType == "SPECIFIED" && | |
| 902 | + SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>() | |
| 903 | + .Where(loc => loc.CategoryId == pc.Id && loc.LocationId == locationId) | |
| 904 | + .Any())))) | |
| 858 | 905 | .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) |
| 859 | 906 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => |
| 860 | 907 | (l.LabelName != null && l.LabelName.Contains(keyword!)) || |
| 861 | 908 | (p.ProductName != null && p.ProductName.Contains(keyword!)) || |
| 862 | 909 | (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) || |
| 910 | + (pc.DisplayText != null && pc.DisplayText.Contains(keyword!)) || | |
| 863 | 911 | (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || |
| 864 | 912 | (t.TypeName != null && t.TypeName.Contains(keyword!)) || |
| 865 | 913 | (l.LabelCode != null && l.LabelCode.Contains(keyword!))); |
| ... | ... | @@ -875,6 +923,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 875 | 923 | |
| 876 | 924 | public string? LabelCategoryPhotoUrl { get; set; } |
| 877 | 925 | |
| 926 | + public string? LabelCategoryButtonAppearance { get; set; } | |
| 927 | + | |
| 878 | 928 | public int LabelCategoryOrderNum { get; set; } |
| 879 | 929 | |
| 880 | 930 | public string? ProductCategoryId { get; set; } |
| ... | ... | @@ -883,6 +933,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 883 | 933 | |
| 884 | 934 | public string? ProductCategoryPhotoUrl { get; set; } |
| 885 | 935 | |
| 936 | + public string? ProductCategoryDisplayText { get; set; } | |
| 937 | + | |
| 938 | + public string? ProductCategoryButtonAppearance { get; set; } | |
| 939 | + | |
| 940 | + public string? ProductCategoryAvailabilityType { get; set; } | |
| 941 | + | |
| 942 | + public int ProductCategoryOrderNum { get; set; } | |
| 943 | + | |
| 886 | 944 | public string ProductId { get; set; } = string.Empty; |
| 887 | 945 | |
| 888 | 946 | public string? ProductName { get; set; } |
| ... | ... | @@ -908,6 +966,32 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 908 | 966 | public string TemplateUnit { get; set; } = "inch"; |
| 909 | 967 | } |
| 910 | 968 | |
| 969 | + /// <summary> | |
| 970 | + /// 将 App 入参中的 JsonElement(对象或 null)反序列化为 PreviewAsync 所需的扁平字典。 | |
| 971 | + /// </summary> | |
| 972 | + private static Dictionary<string, object?>? ParsePrintInputJsonToDictionary(JsonElement? printInputJson) | |
| 973 | + { | |
| 974 | + if (printInputJson is null) | |
| 975 | + { | |
| 976 | + return null; | |
| 977 | + } | |
| 978 | + | |
| 979 | + var je = printInputJson.Value; | |
| 980 | + if (je.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) | |
| 981 | + { | |
| 982 | + return null; | |
| 983 | + } | |
| 984 | + | |
| 985 | + try | |
| 986 | + { | |
| 987 | + return JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText()); | |
| 988 | + } | |
| 989 | + catch | |
| 990 | + { | |
| 991 | + return null; | |
| 992 | + } | |
| 993 | + } | |
| 994 | + | |
| 911 | 995 | private static string NormalizeCategoryName(string? categoryName) |
| 912 | 996 | { |
| 913 | 997 | var s = categoryName?.Trim(); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql
0 → 100644
| 1 | +-- 组织/分组(Group),归属合作伙伴(Parent Partner) | |
| 2 | +-- 依赖:请先执行 fl_partner_create.sql,保证存在 fl_partner 表。 | |
| 3 | + | |
| 4 | +CREATE TABLE IF NOT EXISTS `fl_group` ( | |
| 5 | + `Id` varchar(64) NOT NULL COMMENT '主键', | |
| 6 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 7 | + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', | |
| 8 | + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', | |
| 9 | + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', | |
| 10 | + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', | |
| 11 | + `GroupName` varchar(256) NOT NULL COMMENT '组织名称', | |
| 12 | + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id', | |
| 13 | + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | |
| 14 | + PRIMARY KEY (`Id`), | |
| 15 | + KEY `IX_fl_group_IsDeleted` (`IsDeleted`), | |
| 16 | + KEY `IX_fl_group_State` (`State`), | |
| 17 | + KEY `IX_fl_group_PartnerId` (`PartnerId`), | |
| 18 | + KEY `IX_fl_group_GroupName` (`GroupName`(128)), | |
| 19 | + KEY `IX_fl_group_CreationTime` (`CreationTime`), | |
| 20 | + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`) | |
| 21 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)'; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql
0 → 100644
| 1 | +-- 从旧版「按门店」结构迁移为「全局一条」:删除 LocationId 及门店唯一索引 | |
| 2 | +-- 执行前请确认库中 `fl_location_support` 已存在;若不存在可跳过本脚本,直接使用 fl_location_support_create.sql | |
| 3 | + | |
| 4 | +-- 若存在按门店的唯一索引则删除(名称与历史脚本一致) | |
| 5 | +ALTER TABLE `fl_location_support` DROP INDEX `uk_fl_location_support_locationid`; | |
| 6 | + | |
| 7 | +-- 删除门店列(若列不存在会报错,需按环境调整) | |
| 8 | +ALTER TABLE `fl_location_support` DROP COLUMN `LocationId`; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql
0 → 100644
| 1 | +-- 全局 Support 联系方式(全门店共用一条) | |
| 2 | +CREATE TABLE IF NOT EXISTS `fl_location_support` ( | |
| 3 | + `Id` varchar(50) NOT NULL COMMENT '主键', | |
| 4 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 5 | + `CreationTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | |
| 6 | + `CreatorId` varchar(50) DEFAULT NULL COMMENT '创建人', | |
| 7 | + `LastModifierId` varchar(50) DEFAULT NULL COMMENT '最后修改人', | |
| 8 | + `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间', | |
| 9 | + `SupportPhone` varchar(100) NOT NULL COMMENT 'Support 电话', | |
| 10 | + `SupportEmail` varchar(200) NOT NULL COMMENT 'Support 邮箱', | |
| 11 | + PRIMARY KEY (`Id`), | |
| 12 | + KEY `idx_fl_location_support_isdeleted` (`IsDeleted`) | |
| 13 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='全局 Support 联系方式'; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql
0 → 100644
| 1 | +-- 合作伙伴主数据(美国版 antis-foodlabeling-us) | |
| 2 | +-- 执行前请确认连接库为业务库;若表已存在请勿重复执行。 | |
| 3 | + | |
| 4 | +CREATE TABLE IF NOT EXISTS `fl_partner` ( | |
| 5 | + `Id` varchar(64) NOT NULL COMMENT '主键', | |
| 6 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 7 | + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', | |
| 8 | + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', | |
| 9 | + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', | |
| 10 | + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', | |
| 11 | + `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称', | |
| 12 | + `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱', | |
| 13 | + `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话', | |
| 14 | + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | |
| 15 | + PRIMARY KEY (`Id`), | |
| 16 | + KEY `IX_fl_partner_IsDeleted` (`IsDeleted`), | |
| 17 | + KEY `IX_fl_partner_State` (`State`), | |
| 18 | + KEY `IX_fl_partner_CreationTime` (`CreationTime`), | |
| 19 | + KEY `IX_fl_partner_PartnerName` (`PartnerName`(128)) | |
| 20 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合作伙伴'; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/IServices/IAccountService.cs
| ... | ... | @@ -14,12 +14,6 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices |
| 14 | 14 | Task<bool> RestPasswordAsync(Guid userId, RestPasswordDto input); |
| 15 | 15 | |
| 16 | 16 | /// <summary> |
| 17 | - /// 提供其他服务使用,根据用户id,直接返回token | |
| 18 | - /// </summary> | |
| 19 | - /// <returns></returns> | |
| 20 | - Task<LoginOutputDto> PostLoginAsync(Guid userId); | |
| 21 | - | |
| 22 | - /// <summary> | |
| 23 | 17 | /// 根据信息查询用户,可能为空,代表该用户不存在或禁用 |
| 24 | 18 | /// </summary> |
| 25 | 19 | /// <param name="userName"></param> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/ObjectMapping/RbacMapsterRegister.cs
0 → 100644
| 1 | +using Mapster; | |
| 2 | +using Yi.Framework.Rbac.Application.Contracts.Dtos.Menu; | |
| 3 | +using Yi.Framework.Rbac.Domain.Entities; | |
| 4 | +using Yi.Framework.Rbac.Domain.Shared; | |
| 5 | +using Yi.Framework.Rbac.Domain.Shared.Dtos; | |
| 6 | + | |
| 7 | +namespace Yi.Framework.Rbac.Application.ObjectMapping; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// <c>Menu.ParentId</c> 实体为字符串,对外 DTO 仍为 <see cref="Guid"/>。 | |
| 11 | +/// </summary> | |
| 12 | +public class RbacMapsterRegister : IRegister | |
| 13 | +{ | |
| 14 | + public void Register(TypeAdapterConfig config) | |
| 15 | + { | |
| 16 | + config.NewConfig<MenuAggregateRoot, MenuGetOutputDto>() | |
| 17 | + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId)); | |
| 18 | + | |
| 19 | + config.NewConfig<MenuAggregateRoot, MenuGetListOutputDto>() | |
| 20 | + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId)); | |
| 21 | + | |
| 22 | + // 登录组装用户信息时使用;库中 ParentId 可能为 "0",不能交给 Mapster 默认 string→Guid | |
| 23 | + config.NewConfig<MenuAggregateRoot, MenuDto>() | |
| 24 | + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId)); | |
| 25 | + } | |
| 26 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs
| ... | ... | @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http; |
| 6 | 6 | using Microsoft.AspNetCore.Mvc; |
| 7 | 7 | using Microsoft.Extensions.Caching.Distributed; |
| 8 | 8 | using Microsoft.Extensions.Options; |
| 9 | +using SqlSugar; | |
| 9 | 10 | using Volo.Abp.Application.Services; |
| 10 | 11 | using Volo.Abp.Authorization; |
| 11 | 12 | using Volo.Abp.Caching; |
| ... | ... | @@ -83,7 +84,7 @@ namespace Yi.Framework.Rbac.Application.Services |
| 83 | 84 | //登录不想要验证码 ,可不校验 |
| 84 | 85 | if (!_captcha.Validate(uuid, code)) |
| 85 | 86 | { |
| 86 | - throw new UserFriendlyException("验证码错误"); | |
| 87 | + throw new UserFriendlyException("Invalid captcha."); | |
| 87 | 88 | } |
| 88 | 89 | } |
| 89 | 90 | } |
| ... | ... | @@ -97,21 +98,52 @@ namespace Yi.Framework.Rbac.Application.Services |
| 97 | 98 | [AllowAnonymous] |
| 98 | 99 | public async Task<LoginOutputDto> PostLoginAsync(LoginInputVo input) |
| 99 | 100 | { |
| 100 | - if (string.IsNullOrEmpty(input.Password) || string.IsNullOrEmpty(input.UserName)) | |
| 101 | + var email = input.UserName?.Trim(); | |
| 102 | + var password = input.Password ?? string.Empty; | |
| 103 | + if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password)) | |
| 101 | 104 | { |
| 102 | - throw new UserFriendlyException("请输入合理数据!"); | |
| 105 | + throw new UserFriendlyException("Email and password are required."); | |
| 106 | + } | |
| 107 | + | |
| 108 | + if (!IsPlausibleEmail(email)) | |
| 109 | + { | |
| 110 | + // Platform sign-in supports email only. | |
| 111 | + throw new UserFriendlyException("Sign-in failed: email is required as the account."); | |
| 103 | 112 | } |
| 104 | 113 | |
| 105 | 114 | //校验验证码 |
| 106 | 115 | ValidationImageCaptcha(input.Uuid,input.Code); |
| 107 | 116 | |
| 108 | - UserAggregateRoot user = new(); | |
| 109 | - //校验 | |
| 110 | - await _accountManager.LoginValidationAsync(input.UserName, input.Password, x => user = x); | |
| 117 | + var normalized = email.ToLowerInvariant(); | |
| 118 | + // 平台登录框为「邮箱」:优先按 Email 匹配;若历史账号仅把邮箱形字符串写在 UserName、Email 为空,则按 UserName 匹配。 | |
| 119 | + var candidates = await _userRepository._DbQueryable | |
| 120 | + .Where(u => !u.IsDeleted && u.State == true) | |
| 121 | + .Where(u => | |
| 122 | + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) || | |
| 123 | + SqlFunc.ToLower(u.UserName) == normalized) | |
| 124 | + .ToListAsync(); | |
| 125 | + var user = candidates.FirstOrDefault(u => | |
| 126 | + u.Email != null && | |
| 127 | + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase)) | |
| 128 | + ?? candidates.FirstOrDefault(); | |
| 129 | + if (user is null) | |
| 130 | + { | |
| 131 | + throw new UserFriendlyException("Sign-in failed: account not found."); | |
| 132 | + } | |
| 133 | + | |
| 134 | + if (!user.JudgePassword(password)) | |
| 135 | + { | |
| 136 | + throw new UserFriendlyException("Sign-in failed: incorrect email or password."); | |
| 137 | + } | |
| 111 | 138 | |
| 112 | 139 | return await PostLoginAsync(user.Id); |
| 113 | 140 | } |
| 114 | 141 | |
| 142 | + private static bool IsPlausibleEmail(string email) => | |
| 143 | + email.Contains("@", StringComparison.Ordinal) && | |
| 144 | + !email.StartsWith("@", StringComparison.Ordinal) && | |
| 145 | + !email.EndsWith("@", StringComparison.Ordinal); | |
| 146 | + | |
| 115 | 147 | |
| 116 | 148 | /// <summary> |
| 117 | 149 | /// 提供其他服务使用,根据用户id,直接返回token | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/MenuParentIdConverter.cs
0 → 100644
| 1 | +namespace Yi.Framework.Rbac.Domain.Shared; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// <c>Menu.ParentId</c> 在部分库中为 <c>varchar</c>(如根节点为 <c>0</c>),与 ORM 中 Guid 映射不一致时会导致 SqlSugar 绑定失败;统一用字符串落库并在需要时转为 <see cref="Guid"/>。 | |
| 5 | +/// </summary> | |
| 6 | +public static class MenuParentIdConverter | |
| 7 | +{ | |
| 8 | + public static bool IsRoot(string? raw) => | |
| 9 | + string.IsNullOrWhiteSpace(raw) || | |
| 10 | + raw.Trim() == "0" || | |
| 11 | + string.Equals(raw.Trim(), Guid.Empty.ToString(), StringComparison.OrdinalIgnoreCase); | |
| 12 | + | |
| 13 | + public static Guid ToGuid(string? raw) | |
| 14 | + { | |
| 15 | + if (IsRoot(raw)) | |
| 16 | + { | |
| 17 | + return Guid.Empty; | |
| 18 | + } | |
| 19 | + | |
| 20 | + var t = raw!.Trim(); | |
| 21 | + return Guid.TryParse(t, out var g) ? g : Guid.Empty; | |
| 22 | + } | |
| 23 | + | |
| 24 | + /// <summary>写入数据库:根节点与历史库对齐为 <c>0</c>,否则为标准 GUID 字符串。</summary> | |
| 25 | + public static string FromGuid(Guid g) => g == Guid.Empty ? "0" : g.ToString("D"); | |
| 26 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/MenuAggregateRoot.cs
| 1 | -using System.Text.RegularExpressions; | |
| 1 | +using System.Text.RegularExpressions; | |
| 2 | 2 | using System.Web; |
| 3 | 3 | using NUglify.Helpers; |
| 4 | 4 | using SqlSugar; |
| ... | ... | @@ -7,6 +7,7 @@ using Volo.Abp.Auditing; |
| 7 | 7 | using Volo.Abp.Domain.Entities; |
| 8 | 8 | using Yi.Framework.Core.Data; |
| 9 | 9 | using Yi.Framework.Core.Helper; |
| 10 | +using Yi.Framework.Rbac.Domain.Shared; | |
| 10 | 11 | using Yi.Framework.Rbac.Domain.Shared.Dtos; |
| 11 | 12 | using Yi.Framework.Rbac.Domain.Shared.Enums; |
| 12 | 13 | |
| ... | ... | @@ -25,13 +26,13 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 25 | 26 | public MenuAggregateRoot(Guid id) |
| 26 | 27 | { |
| 27 | 28 | Id = id; |
| 28 | - ParentId = Guid.Empty; | |
| 29 | + ParentId = MenuParentIdConverter.FromGuid(Guid.Empty); | |
| 29 | 30 | } |
| 30 | 31 | |
| 31 | 32 | public MenuAggregateRoot(Guid id, Guid parentId) |
| 32 | 33 | { |
| 33 | 34 | Id = id; |
| 34 | - ParentId = parentId; | |
| 35 | + ParentId = MenuParentIdConverter.FromGuid(parentId); | |
| 35 | 36 | } |
| 36 | 37 | |
| 37 | 38 | /// <summary> |
| ... | ... | @@ -100,8 +101,9 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 100 | 101 | /// <summary> |
| 101 | 102 | /// |
| 102 | 103 | ///</summary> |
| 104 | + /// <summary>父级菜单 Id;库中多为 varchar(如根为 <c>0</c> 或 GUID 文本)。</summary> | |
| 103 | 105 | [SugarColumn(ColumnName = "ParentId")] |
| 104 | - public Guid ParentId { get; set; } | |
| 106 | + public string ParentId { get; set; } = "0"; | |
| 105 | 107 | |
| 106 | 108 | /// <summary> |
| 107 | 109 | /// 菜单图标 |
| ... | ... | @@ -183,7 +185,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 183 | 185 | r.OrderNum = m.OrderNum; |
| 184 | 186 | var routerName = m.Router?.Split("/").LastOrDefault(); |
| 185 | 187 | r.Id = m.Id; |
| 186 | - r.ParentId = m.ParentId; | |
| 188 | + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId); | |
| 187 | 189 | |
| 188 | 190 | //开头大写 |
| 189 | 191 | r.Name = routerName?.First().ToString().ToUpper() + routerName?.Substring(1); |
| ... | ... | @@ -197,7 +199,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 197 | 199 | r.AlwaysShow = true; |
| 198 | 200 | |
| 199 | 201 | //判断是否为最顶层的路由 |
| 200 | - if (Guid.Empty == m.ParentId) | |
| 202 | + if (MenuParentIdConverter.IsRoot(m.ParentId)) | |
| 201 | 203 | { |
| 202 | 204 | r.Component = "Layout"; |
| 203 | 205 | } |
| ... | ... | @@ -250,7 +252,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 250 | 252 | var r = new Vue3RouterDto(); |
| 251 | 253 | r.OrderNum = m.OrderNum; |
| 252 | 254 | r.Id = m.Id; |
| 253 | - r.ParentId = m.ParentId; | |
| 255 | + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId); | |
| 254 | 256 | r.Hidden = !m.IsShow; |
| 255 | 257 | |
| 256 | 258 | // 检测是否为 URL 链接(http:// 或 https:// 开头) |
| ... | ... | @@ -359,7 +361,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 359 | 361 | r.AlwaysShow = false; |
| 360 | 362 | |
| 361 | 363 | // 判断是否为最顶层的路由 |
| 362 | - if (Guid.Empty == m.ParentId) | |
| 364 | + if (MenuParentIdConverter.IsRoot(m.ParentId)) | |
| 363 | 365 | { |
| 364 | 366 | r.Component = "Layout"; |
| 365 | 367 | } |
| ... | ... | @@ -385,7 +387,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 385 | 387 | r.AlwaysShow = true; |
| 386 | 388 | |
| 387 | 389 | // 判断是否为最顶层的路由 |
| 388 | - if (Guid.Empty == m.ParentId) | |
| 390 | + if (MenuParentIdConverter.IsRoot(m.ParentId)) | |
| 389 | 391 | { |
| 390 | 392 | r.Component = "Layout"; |
| 391 | 393 | } |
| ... | ... | @@ -449,7 +451,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 449 | 451 | }, |
| 450 | 452 | Children =null, |
| 451 | 453 | Id = m.Id, |
| 452 | - ParentId = m.ParentId | |
| 454 | + ParentId = MenuParentIdConverter.ToGuid(m.ParentId) | |
| 453 | 455 | }) |
| 454 | 456 | .ToList(); |
| 455 | 457 | |
| ... | ... | @@ -487,7 +489,7 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 487 | 489 | var treeDto = new MenuTreeDto |
| 488 | 490 | { |
| 489 | 491 | Id = m.Id, |
| 490 | - ParentId = m.ParentId, | |
| 492 | + ParentId = MenuParentIdConverter.ToGuid(m.ParentId), | |
| 491 | 493 | OrderNum = m.OrderNum, |
| 492 | 494 | MenuName = m.MenuName, |
| 493 | 495 | MenuType = m.MenuType, | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs
| 1 | -using System.IdentityModel.Tokens.Jwt; | |
| 1 | +using System.IdentityModel.Tokens.Jwt; | |
| 2 | 2 | using System.Security.Claims; |
| 3 | 3 | using System.Text; |
| 4 | 4 | using Mapster; |
| ... | ... | @@ -77,10 +77,8 @@ namespace Yi.Framework.Rbac.Domain.Managers |
| 77 | 77 | { |
| 78 | 78 | throw new UserFriendlyException(UserConst.No_Role); |
| 79 | 79 | } |
| 80 | - if (!userInfo.PermissionCodes.Any()) | |
| 81 | - { | |
| 82 | - throw new UserFriendlyException(UserConst.No_Permission); | |
| 83 | - } | |
| 80 | + | |
| 81 | + // 菜单表 PermissionCode 可未落库;当前以角色及角色-菜单绑定为准,不要求 PermissionCodes 非空 | |
| 84 | 82 | |
| 85 | 83 | if (getUserInfo is not null) |
| 86 | 84 | { | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/UserManager.cs
| 1 | -using System.Text.RegularExpressions; | |
| 1 | +using System.Text.RegularExpressions; | |
| 2 | 2 | using Mapster; |
| 3 | 3 | using Microsoft.Extensions.Caching.Distributed; |
| 4 | 4 | using Microsoft.Extensions.DependencyInjection; |
| ... | ... | @@ -10,6 +10,7 @@ using Volo.Abp.EventBus.Local; |
| 10 | 10 | using Volo.Abp.Guids; |
| 11 | 11 | using Yi.Framework.Rbac.Domain.Entities; |
| 12 | 12 | using Yi.Framework.Rbac.Domain.Repositories; |
| 13 | +using Yi.Framework.Rbac.Domain.Shared; | |
| 13 | 14 | using Yi.Framework.Rbac.Domain.Shared.Caches; |
| 14 | 15 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| 15 | 16 | using Yi.Framework.Rbac.Domain.Shared.Dtos; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuPureDataSeed.cs
| ... | ... | @@ -87,7 +87,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 87 | 87 | Router = "/system/user/index", |
| 88 | 88 | MenuIcon = "ri:admin-line", |
| 89 | 89 | OrderNum = 100, |
| 90 | - ParentId = system.Id, | |
| 90 | + ParentId = system.Id.ToString(), | |
| 91 | 91 | RouterName = "SystemUser" |
| 92 | 92 | }; |
| 93 | 93 | entities.Add(user); |
| ... | ... | @@ -99,7 +99,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 99 | 99 | PermissionCode = "system:user:query", |
| 100 | 100 | MenuType = MenuTypeEnum.Component, |
| 101 | 101 | OrderNum = 100, |
| 102 | - ParentId = user.Id, | |
| 102 | + ParentId = user.Id.ToString(), | |
| 103 | 103 | IsDeleted = false |
| 104 | 104 | }; |
| 105 | 105 | entities.Add(userQuery); |
| ... | ... | @@ -111,7 +111,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 111 | 111 | PermissionCode = "system:user:add", |
| 112 | 112 | MenuType = MenuTypeEnum.Component, |
| 113 | 113 | OrderNum = 100, |
| 114 | - ParentId = user.Id, | |
| 114 | + ParentId = user.Id.ToString(), | |
| 115 | 115 | IsDeleted = false |
| 116 | 116 | }; |
| 117 | 117 | entities.Add(userAdd); |
| ... | ... | @@ -123,7 +123,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 123 | 123 | PermissionCode = "system:user:edit", |
| 124 | 124 | MenuType = MenuTypeEnum.Component, |
| 125 | 125 | OrderNum = 100, |
| 126 | - ParentId = user.Id, | |
| 126 | + ParentId = user.Id.ToString(), | |
| 127 | 127 | IsDeleted = false |
| 128 | 128 | }; |
| 129 | 129 | entities.Add(userEdit); |
| ... | ... | @@ -135,7 +135,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 135 | 135 | PermissionCode = "system:user:remove", |
| 136 | 136 | MenuType = MenuTypeEnum.Component, |
| 137 | 137 | OrderNum = 100, |
| 138 | - ParentId = user.Id, | |
| 138 | + ParentId = user.Id.ToString(), | |
| 139 | 139 | IsDeleted = false |
| 140 | 140 | }; |
| 141 | 141 | entities.Add(userRemove); |
| ... | ... | @@ -148,7 +148,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 148 | 148 | PermissionCode = "system:user:resetPwd", |
| 149 | 149 | MenuType = MenuTypeEnum.Component, |
| 150 | 150 | OrderNum = 100, |
| 151 | - ParentId = user.Id, | |
| 151 | + ParentId = user.Id.ToString(), | |
| 152 | 152 | IsDeleted = false |
| 153 | 153 | }; |
| 154 | 154 | entities.Add(userResetPwd); |
| ... | ... | @@ -164,7 +164,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 164 | 164 | Router = "/system/role/index", |
| 165 | 165 | MenuIcon = "ri:admin-fill", |
| 166 | 166 | OrderNum = 99, |
| 167 | - ParentId = system.Id, | |
| 167 | + ParentId = system.Id.ToString(), | |
| 168 | 168 | RouterName = "SystemRole" |
| 169 | 169 | }; |
| 170 | 170 | entities.Add(role); |
| ... | ... | @@ -176,7 +176,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 176 | 176 | PermissionCode = "system:role:query", |
| 177 | 177 | MenuType = MenuTypeEnum.Component, |
| 178 | 178 | OrderNum = 100, |
| 179 | - ParentId = role.Id, | |
| 179 | + ParentId = role.Id.ToString(), | |
| 180 | 180 | IsDeleted = false |
| 181 | 181 | }; |
| 182 | 182 | entities.Add(roleQuery); |
| ... | ... | @@ -188,7 +188,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 188 | 188 | PermissionCode = "system:role:add", |
| 189 | 189 | MenuType = MenuTypeEnum.Component, |
| 190 | 190 | OrderNum = 100, |
| 191 | - ParentId = role.Id, | |
| 191 | + ParentId = role.Id.ToString(), | |
| 192 | 192 | IsDeleted = false |
| 193 | 193 | }; |
| 194 | 194 | entities.Add(roleAdd); |
| ... | ... | @@ -200,7 +200,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 200 | 200 | PermissionCode = "system:role:edit", |
| 201 | 201 | MenuType = MenuTypeEnum.Component, |
| 202 | 202 | OrderNum = 100, |
| 203 | - ParentId = role.Id, | |
| 203 | + ParentId = role.Id.ToString(), | |
| 204 | 204 | IsDeleted = false |
| 205 | 205 | }; |
| 206 | 206 | entities.Add(roleEdit); |
| ... | ... | @@ -212,7 +212,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 212 | 212 | PermissionCode = "system:role:remove", |
| 213 | 213 | MenuType = MenuTypeEnum.Component, |
| 214 | 214 | OrderNum = 100, |
| 215 | - ParentId = role.Id, | |
| 215 | + ParentId = role.Id.ToString(), | |
| 216 | 216 | IsDeleted = false |
| 217 | 217 | }; |
| 218 | 218 | entities.Add(roleRemove); |
| ... | ... | @@ -228,7 +228,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 228 | 228 | Router = "/system/menu/index", |
| 229 | 229 | MenuIcon = "ep:menu", |
| 230 | 230 | OrderNum = 98, |
| 231 | - ParentId = system.Id, | |
| 231 | + ParentId = system.Id.ToString(), | |
| 232 | 232 | RouterName = "SystemMenu" |
| 233 | 233 | }; |
| 234 | 234 | entities.Add(menu); |
| ... | ... | @@ -240,7 +240,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 240 | 240 | PermissionCode = "system:menu:query", |
| 241 | 241 | MenuType = MenuTypeEnum.Component, |
| 242 | 242 | OrderNum = 100, |
| 243 | - ParentId = menu.Id, | |
| 243 | + ParentId = menu.Id.ToString(), | |
| 244 | 244 | IsDeleted = false |
| 245 | 245 | }; |
| 246 | 246 | entities.Add(menuQuery); |
| ... | ... | @@ -252,7 +252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 252 | 252 | PermissionCode = "system:menu:add", |
| 253 | 253 | MenuType = MenuTypeEnum.Component, |
| 254 | 254 | OrderNum = 100, |
| 255 | - ParentId = menu.Id, | |
| 255 | + ParentId = menu.Id.ToString(), | |
| 256 | 256 | IsDeleted = false |
| 257 | 257 | }; |
| 258 | 258 | entities.Add(menuAdd); |
| ... | ... | @@ -264,7 +264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 264 | 264 | PermissionCode = "system:menu:edit", |
| 265 | 265 | MenuType = MenuTypeEnum.Component, |
| 266 | 266 | OrderNum = 100, |
| 267 | - ParentId = menu.Id, | |
| 267 | + ParentId = menu.Id.ToString(), | |
| 268 | 268 | IsDeleted = false |
| 269 | 269 | }; |
| 270 | 270 | entities.Add(menuEdit); |
| ... | ... | @@ -276,7 +276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 276 | 276 | PermissionCode = "system:menu:remove", |
| 277 | 277 | MenuType = MenuTypeEnum.Component, |
| 278 | 278 | OrderNum = 100, |
| 279 | - ParentId = menu.Id, | |
| 279 | + ParentId = menu.Id.ToString(), | |
| 280 | 280 | IsDeleted = false |
| 281 | 281 | }; |
| 282 | 282 | entities.Add(menuRemove); |
| ... | ... | @@ -291,7 +291,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 291 | 291 | Router = "/system/dept/index", |
| 292 | 292 | MenuIcon = "ri:git-branch-line", |
| 293 | 293 | OrderNum = 97, |
| 294 | - ParentId = system.Id, | |
| 294 | + ParentId = system.Id.ToString(), | |
| 295 | 295 | RouterName = "SystemDept" |
| 296 | 296 | }; |
| 297 | 297 | entities.Add(dept); |
| ... | ... | @@ -303,7 +303,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 303 | 303 | PermissionCode = "system:dept:query", |
| 304 | 304 | MenuType = MenuTypeEnum.Component, |
| 305 | 305 | OrderNum = 100, |
| 306 | - ParentId = dept.Id, | |
| 306 | + ParentId = dept.Id.ToString(), | |
| 307 | 307 | IsDeleted = false |
| 308 | 308 | }; |
| 309 | 309 | entities.Add(deptQuery); |
| ... | ... | @@ -315,7 +315,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 315 | 315 | PermissionCode = "system:dept:add", |
| 316 | 316 | MenuType = MenuTypeEnum.Component, |
| 317 | 317 | OrderNum = 100, |
| 318 | - ParentId = dept.Id, | |
| 318 | + ParentId = dept.Id.ToString(), | |
| 319 | 319 | IsDeleted = false |
| 320 | 320 | }; |
| 321 | 321 | entities.Add(deptAdd); |
| ... | ... | @@ -327,7 +327,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 327 | 327 | PermissionCode = "system:dept:edit", |
| 328 | 328 | MenuType = MenuTypeEnum.Component, |
| 329 | 329 | OrderNum = 100, |
| 330 | - ParentId = dept.Id, | |
| 330 | + ParentId = dept.Id.ToString(), | |
| 331 | 331 | IsDeleted = false |
| 332 | 332 | }; |
| 333 | 333 | entities.Add(deptEdit); |
| ... | ... | @@ -339,7 +339,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 339 | 339 | PermissionCode = "system:dept:remove", |
| 340 | 340 | MenuType = MenuTypeEnum.Component, |
| 341 | 341 | OrderNum = 100, |
| 342 | - ParentId = dept.Id, | |
| 342 | + ParentId = dept.Id.ToString(), | |
| 343 | 343 | IsDeleted = false |
| 344 | 344 | }; |
| 345 | 345 | entities.Add(deptRemove); |
| ... | ... | @@ -356,7 +356,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 356 | 356 | Router = "/system/post/index", |
| 357 | 357 | MenuIcon = "ant-design:deployment-unit-outlined", |
| 358 | 358 | OrderNum = 96, |
| 359 | - ParentId = system.Id, | |
| 359 | + ParentId = system.Id.ToString(), | |
| 360 | 360 | RouterName = "SystemPost" |
| 361 | 361 | }; |
| 362 | 362 | entities.Add(post); |
| ... | ... | @@ -368,7 +368,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 368 | 368 | PermissionCode = "system:post:query", |
| 369 | 369 | MenuType = MenuTypeEnum.Component, |
| 370 | 370 | OrderNum = 100, |
| 371 | - ParentId = post.Id, | |
| 371 | + ParentId = post.Id.ToString(), | |
| 372 | 372 | IsDeleted = false |
| 373 | 373 | }; |
| 374 | 374 | entities.Add(postQuery); |
| ... | ... | @@ -380,7 +380,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 380 | 380 | PermissionCode = "system:post:add", |
| 381 | 381 | MenuType = MenuTypeEnum.Component, |
| 382 | 382 | OrderNum = 100, |
| 383 | - ParentId = post.Id, | |
| 383 | + ParentId = post.Id.ToString(), | |
| 384 | 384 | IsDeleted = false |
| 385 | 385 | }; |
| 386 | 386 | entities.Add(postAdd); |
| ... | ... | @@ -392,7 +392,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 392 | 392 | PermissionCode = "system:post:edit", |
| 393 | 393 | MenuType = MenuTypeEnum.Component, |
| 394 | 394 | OrderNum = 100, |
| 395 | - ParentId = post.Id, | |
| 395 | + ParentId = post.Id.ToString(), | |
| 396 | 396 | IsDeleted = false |
| 397 | 397 | }; |
| 398 | 398 | entities.Add(postEdit); |
| ... | ... | @@ -404,7 +404,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 404 | 404 | PermissionCode = "system:post:remove", |
| 405 | 405 | MenuType = MenuTypeEnum.Component, |
| 406 | 406 | OrderNum = 100, |
| 407 | - ParentId = post.Id, | |
| 407 | + ParentId = post.Id.ToString(), | |
| 408 | 408 | IsDeleted = false |
| 409 | 409 | }; |
| 410 | 410 | entities.Add(postRemove); |
| ... | ... | @@ -420,7 +420,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 420 | 420 | Router = "/monitor/operation-logs", |
| 421 | 421 | MenuIcon = "ri:history-fill", |
| 422 | 422 | OrderNum = 100, |
| 423 | - ParentId = monitoring.Id, | |
| 423 | + ParentId = monitoring.Id.ToString(), | |
| 424 | 424 | RouterName = "OperationLog", |
| 425 | 425 | Component = "monitor/logs/operation/index" |
| 426 | 426 | }; |
| ... | ... | @@ -433,7 +433,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 433 | 433 | PermissionCode = "monitor:operlog:query", |
| 434 | 434 | MenuType = MenuTypeEnum.Component, |
| 435 | 435 | OrderNum = 100, |
| 436 | - ParentId = operationLog.Id, | |
| 436 | + ParentId = operationLog.Id.ToString(), | |
| 437 | 437 | IsDeleted = false |
| 438 | 438 | }; |
| 439 | 439 | entities.Add(operationLogQuery); |
| ... | ... | @@ -445,7 +445,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 445 | 445 | PermissionCode = "monitor:operlog:remove", |
| 446 | 446 | MenuType = MenuTypeEnum.Component, |
| 447 | 447 | OrderNum = 100, |
| 448 | - ParentId = operationLog.Id, | |
| 448 | + ParentId = operationLog.Id.ToString(), | |
| 449 | 449 | IsDeleted = false |
| 450 | 450 | }; |
| 451 | 451 | entities.Add(operationLogRemove); |
| ... | ... | @@ -465,7 +465,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 465 | 465 | Component = "monitor/logs/login/index", |
| 466 | 466 | MenuIcon = "ri:window-line", |
| 467 | 467 | OrderNum = 100, |
| 468 | - ParentId = monitoring.Id, | |
| 468 | + ParentId = monitoring.Id.ToString(), | |
| 469 | 469 | RouterName = "LoginLog", |
| 470 | 470 | }; |
| 471 | 471 | entities.Add(loginLog); |
| ... | ... | @@ -477,7 +477,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 477 | 477 | PermissionCode = "monitor:logininfor:query", |
| 478 | 478 | MenuType = MenuTypeEnum.Component, |
| 479 | 479 | OrderNum = 100, |
| 480 | - ParentId = loginLog.Id, | |
| 480 | + ParentId = loginLog.Id.ToString(), | |
| 481 | 481 | IsDeleted = false |
| 482 | 482 | }; |
| 483 | 483 | entities.Add(loginLogQuery); |
| ... | ... | @@ -489,7 +489,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 489 | 489 | PermissionCode = "monitor:logininfor:remove", |
| 490 | 490 | MenuType = MenuTypeEnum.Component, |
| 491 | 491 | OrderNum = 100, |
| 492 | - ParentId = loginLog.Id, | |
| 492 | + ParentId = loginLog.Id.ToString(), | |
| 493 | 493 | IsDeleted = false, |
| 494 | 494 | |
| 495 | 495 | }; |
| ... | ... | @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 509 | 509 | Component = "/system/config/index", |
| 510 | 510 | MenuIcon = "ri:edit-box-line", |
| 511 | 511 | OrderNum = 94, |
| 512 | - ParentId = system.Id, | |
| 512 | + ParentId = system.Id.ToString(), | |
| 513 | 513 | IsDeleted = false |
| 514 | 514 | }; |
| 515 | 515 | entities.Add(config); |
| ... | ... | @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 521 | 521 | PermissionCode = "system:config:query", |
| 522 | 522 | MenuType = MenuTypeEnum.Component, |
| 523 | 523 | OrderNum = 100, |
| 524 | - ParentId = config.Id, | |
| 524 | + ParentId = config.Id.ToString(), | |
| 525 | 525 | IsDeleted = false |
| 526 | 526 | }; |
| 527 | 527 | entities.Add(configQuery); |
| ... | ... | @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 533 | 533 | PermissionCode = "system:config:add", |
| 534 | 534 | MenuType = MenuTypeEnum.Component, |
| 535 | 535 | OrderNum = 100, |
| 536 | - ParentId = config.Id, | |
| 536 | + ParentId = config.Id.ToString(), | |
| 537 | 537 | IsDeleted = false |
| 538 | 538 | }; |
| 539 | 539 | entities.Add(configAdd); |
| ... | ... | @@ -545,7 +545,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 545 | 545 | PermissionCode = "system:config:edit", |
| 546 | 546 | MenuType = MenuTypeEnum.Component, |
| 547 | 547 | OrderNum = 100, |
| 548 | - ParentId = config.Id, | |
| 548 | + ParentId = config.Id.ToString(), | |
| 549 | 549 | IsDeleted = false |
| 550 | 550 | }; |
| 551 | 551 | entities.Add(configEdit); |
| ... | ... | @@ -557,7 +557,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 557 | 557 | PermissionCode = "system:config:remove", |
| 558 | 558 | MenuType = MenuTypeEnum.Component, |
| 559 | 559 | OrderNum = 100, |
| 560 | - ParentId = config.Id, | |
| 560 | + ParentId = config.Id.ToString(), | |
| 561 | 561 | IsDeleted = false |
| 562 | 562 | }; |
| 563 | 563 | entities.Add(configRemove); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuRuoYiDataSeed.cs
| ... | ... | @@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 88 | 88 | Component = "code/field/index", |
| 89 | 89 | MenuIcon = "number", |
| 90 | 90 | OrderNum = 99, |
| 91 | - ParentId = code.Id, | |
| 91 | + ParentId = code.Id.ToString(), | |
| 92 | 92 | IsDeleted = false |
| 93 | 93 | }; |
| 94 | 94 | entities.Add(field); |
| ... | ... | @@ -599,7 +599,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 599 | 599 | Component = "system/tenant/index", |
| 600 | 600 | MenuIcon = "list", |
| 601 | 601 | OrderNum = 101, |
| 602 | - ParentId = system.Id, | |
| 602 | + ParentId = system.Id.ToString(), | |
| 603 | 603 | IsDeleted = false |
| 604 | 604 | }; |
| 605 | 605 | entities.Add(tenant); |
| ... | ... | @@ -611,7 +611,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 611 | 611 | PermissionCode = "system:tenant:query", |
| 612 | 612 | MenuType = MenuTypeEnum.Component, |
| 613 | 613 | OrderNum = 100, |
| 614 | - ParentId = tenant.Id, | |
| 614 | + ParentId = tenant.Id.ToString(), | |
| 615 | 615 | IsDeleted = false |
| 616 | 616 | }; |
| 617 | 617 | entities.Add(tenantQuery); |
| ... | ... | @@ -623,7 +623,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 623 | 623 | PermissionCode = "system:tenant:add", |
| 624 | 624 | MenuType = MenuTypeEnum.Component, |
| 625 | 625 | OrderNum = 100, |
| 626 | - ParentId = tenant.Id, | |
| 626 | + ParentId = tenant.Id.ToString(), | |
| 627 | 627 | IsDeleted = false |
| 628 | 628 | }; |
| 629 | 629 | entities.Add(tenantAdd); |
| ... | ... | @@ -635,7 +635,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 635 | 635 | PermissionCode = "system:tenant:edit", |
| 636 | 636 | MenuType = MenuTypeEnum.Component, |
| 637 | 637 | OrderNum = 100, |
| 638 | - ParentId = tenant.Id, | |
| 638 | + ParentId = tenant.Id.ToString(), | |
| 639 | 639 | IsDeleted = false |
| 640 | 640 | }; |
| 641 | 641 | entities.Add(tenantEdit); |
| ... | ... | @@ -647,7 +647,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 647 | 647 | PermissionCode = "system:tenant:remove", |
| 648 | 648 | MenuType = MenuTypeEnum.Component, |
| 649 | 649 | OrderNum = 100, |
| 650 | - ParentId = tenant.Id, | |
| 650 | + ParentId = tenant.Id.ToString(), | |
| 651 | 651 | IsDeleted = false |
| 652 | 652 | }; |
| 653 | 653 | entities.Add(tenantRemove); |
| ... | ... | @@ -677,7 +677,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 677 | 677 | Component = "system/user/index", |
| 678 | 678 | MenuIcon = "user", |
| 679 | 679 | OrderNum = 100, |
| 680 | - ParentId = system.Id, | |
| 680 | + ParentId = system.Id.ToString(), | |
| 681 | 681 | IsDeleted = false |
| 682 | 682 | }; |
| 683 | 683 | entities.Add(user); |
| ... | ... | @@ -689,7 +689,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 689 | 689 | PermissionCode = "system:user:query", |
| 690 | 690 | MenuType = MenuTypeEnum.Component, |
| 691 | 691 | OrderNum = 100, |
| 692 | - ParentId = user.Id, | |
| 692 | + ParentId = user.Id.ToString(), | |
| 693 | 693 | IsDeleted = false |
| 694 | 694 | }; |
| 695 | 695 | entities.Add(userQuery); |
| ... | ... | @@ -701,7 +701,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 701 | 701 | PermissionCode = "system:user:add", |
| 702 | 702 | MenuType = MenuTypeEnum.Component, |
| 703 | 703 | OrderNum = 100, |
| 704 | - ParentId = user.Id, | |
| 704 | + ParentId = user.Id.ToString(), | |
| 705 | 705 | IsDeleted = false |
| 706 | 706 | }; |
| 707 | 707 | entities.Add(userAdd); |
| ... | ... | @@ -713,7 +713,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 713 | 713 | PermissionCode = "system:user:edit", |
| 714 | 714 | MenuType = MenuTypeEnum.Component, |
| 715 | 715 | OrderNum = 100, |
| 716 | - ParentId = user.Id, | |
| 716 | + ParentId = user.Id.ToString(), | |
| 717 | 717 | IsDeleted = false |
| 718 | 718 | }; |
| 719 | 719 | entities.Add(userEdit); |
| ... | ... | @@ -725,7 +725,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 725 | 725 | PermissionCode = "system:user:remove", |
| 726 | 726 | MenuType = MenuTypeEnum.Component, |
| 727 | 727 | OrderNum = 100, |
| 728 | - ParentId = user.Id, | |
| 728 | + ParentId = user.Id.ToString(), | |
| 729 | 729 | IsDeleted = false |
| 730 | 730 | }; |
| 731 | 731 | entities.Add(userRemove); |
| ... | ... | @@ -738,7 +738,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 738 | 738 | PermissionCode = "system:user:resetPwd", |
| 739 | 739 | MenuType = MenuTypeEnum.Component, |
| 740 | 740 | OrderNum = 100, |
| 741 | - ParentId = user.Id, | |
| 741 | + ParentId = user.Id.ToString(), | |
| 742 | 742 | IsDeleted = false |
| 743 | 743 | }; |
| 744 | 744 | entities.Add(userResetPwd); |
| ... | ... | @@ -758,7 +758,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 758 | 758 | Component = "system/role/index", |
| 759 | 759 | MenuIcon = "peoples", |
| 760 | 760 | OrderNum = 99, |
| 761 | - ParentId = system.Id, | |
| 761 | + ParentId = system.Id.ToString(), | |
| 762 | 762 | IsDeleted = false |
| 763 | 763 | }; |
| 764 | 764 | entities.Add(role); |
| ... | ... | @@ -770,7 +770,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 770 | 770 | PermissionCode = "system:role:query", |
| 771 | 771 | MenuType = MenuTypeEnum.Component, |
| 772 | 772 | OrderNum = 100, |
| 773 | - ParentId = role.Id, | |
| 773 | + ParentId = role.Id.ToString(), | |
| 774 | 774 | IsDeleted = false |
| 775 | 775 | }; |
| 776 | 776 | entities.Add(roleQuery); |
| ... | ... | @@ -782,7 +782,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 782 | 782 | PermissionCode = "system:role:add", |
| 783 | 783 | MenuType = MenuTypeEnum.Component, |
| 784 | 784 | OrderNum = 100, |
| 785 | - ParentId = role.Id, | |
| 785 | + ParentId = role.Id.ToString(), | |
| 786 | 786 | IsDeleted = false |
| 787 | 787 | }; |
| 788 | 788 | entities.Add(roleAdd); |
| ... | ... | @@ -794,7 +794,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 794 | 794 | PermissionCode = "system:role:edit", |
| 795 | 795 | MenuType = MenuTypeEnum.Component, |
| 796 | 796 | OrderNum = 100, |
| 797 | - ParentId = role.Id, | |
| 797 | + ParentId = role.Id.ToString(), | |
| 798 | 798 | IsDeleted = false |
| 799 | 799 | }; |
| 800 | 800 | entities.Add(roleEdit); |
| ... | ... | @@ -806,7 +806,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 806 | 806 | PermissionCode = "system:role:remove", |
| 807 | 807 | MenuType = MenuTypeEnum.Component, |
| 808 | 808 | OrderNum = 100, |
| 809 | - ParentId = role.Id, | |
| 809 | + ParentId = role.Id.ToString(), | |
| 810 | 810 | IsDeleted = false |
| 811 | 811 | }; |
| 812 | 812 | entities.Add(roleRemove); |
| ... | ... | @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 826 | 826 | Component = "system/menu/index", |
| 827 | 827 | MenuIcon = "tree-table", |
| 828 | 828 | OrderNum = 98, |
| 829 | - ParentId = system.Id, | |
| 829 | + ParentId = system.Id.ToString(), | |
| 830 | 830 | IsDeleted = false |
| 831 | 831 | }; |
| 832 | 832 | entities.Add(menu); |
| ... | ... | @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 838 | 838 | PermissionCode = "system:menu:query", |
| 839 | 839 | MenuType = MenuTypeEnum.Component, |
| 840 | 840 | OrderNum = 100, |
| 841 | - ParentId = menu.Id, | |
| 841 | + ParentId = menu.Id.ToString(), | |
| 842 | 842 | IsDeleted = false |
| 843 | 843 | }; |
| 844 | 844 | entities.Add(menuQuery); |
| ... | ... | @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 850 | 850 | PermissionCode = "system:menu:add", |
| 851 | 851 | MenuType = MenuTypeEnum.Component, |
| 852 | 852 | OrderNum = 100, |
| 853 | - ParentId = menu.Id, | |
| 853 | + ParentId = menu.Id.ToString(), | |
| 854 | 854 | IsDeleted = false |
| 855 | 855 | }; |
| 856 | 856 | entities.Add(menuAdd); |
| ... | ... | @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 862 | 862 | PermissionCode = "system:menu:edit", |
| 863 | 863 | MenuType = MenuTypeEnum.Component, |
| 864 | 864 | OrderNum = 100, |
| 865 | - ParentId = menu.Id, | |
| 865 | + ParentId = menu.Id.ToString(), | |
| 866 | 866 | IsDeleted = false |
| 867 | 867 | }; |
| 868 | 868 | entities.Add(menuEdit); |
| ... | ... | @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 874 | 874 | PermissionCode = "system:menu:remove", |
| 875 | 875 | MenuType = MenuTypeEnum.Component, |
| 876 | 876 | OrderNum = 100, |
| 877 | - ParentId = menu.Id, | |
| 877 | + ParentId = menu.Id.ToString(), | |
| 878 | 878 | IsDeleted = false |
| 879 | 879 | }; |
| 880 | 880 | entities.Add(menuRemove); |
| ... | ... | @@ -893,7 +893,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 893 | 893 | Component = "system/dept/index", |
| 894 | 894 | MenuIcon = "tree", |
| 895 | 895 | OrderNum = 97, |
| 896 | - ParentId = system.Id, | |
| 896 | + ParentId = system.Id.ToString(), | |
| 897 | 897 | IsDeleted = false |
| 898 | 898 | }; |
| 899 | 899 | entities.Add(dept); |
| ... | ... | @@ -905,7 +905,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 905 | 905 | PermissionCode = "system:dept:query", |
| 906 | 906 | MenuType = MenuTypeEnum.Component, |
| 907 | 907 | OrderNum = 100, |
| 908 | - ParentId = dept.Id, | |
| 908 | + ParentId = dept.Id.ToString(), | |
| 909 | 909 | IsDeleted = false |
| 910 | 910 | }; |
| 911 | 911 | entities.Add(deptQuery); |
| ... | ... | @@ -917,7 +917,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 917 | 917 | PermissionCode = "system:dept:add", |
| 918 | 918 | MenuType = MenuTypeEnum.Component, |
| 919 | 919 | OrderNum = 100, |
| 920 | - ParentId = dept.Id, | |
| 920 | + ParentId = dept.Id.ToString(), | |
| 921 | 921 | IsDeleted = false |
| 922 | 922 | }; |
| 923 | 923 | entities.Add(deptAdd); |
| ... | ... | @@ -929,7 +929,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 929 | 929 | PermissionCode = "system:dept:edit", |
| 930 | 930 | MenuType = MenuTypeEnum.Component, |
| 931 | 931 | OrderNum = 100, |
| 932 | - ParentId = dept.Id, | |
| 932 | + ParentId = dept.Id.ToString(), | |
| 933 | 933 | IsDeleted = false |
| 934 | 934 | }; |
| 935 | 935 | entities.Add(deptEdit); |
| ... | ... | @@ -941,7 +941,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 941 | 941 | PermissionCode = "system:dept:remove", |
| 942 | 942 | MenuType = MenuTypeEnum.Component, |
| 943 | 943 | OrderNum = 100, |
| 944 | - ParentId = dept.Id, | |
| 944 | + ParentId = dept.Id.ToString(), | |
| 945 | 945 | IsDeleted = false |
| 946 | 946 | }; |
| 947 | 947 | entities.Add(deptRemove); |
| ... | ... | @@ -962,7 +962,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 962 | 962 | Component = "system/post/index", |
| 963 | 963 | MenuIcon = "post", |
| 964 | 964 | OrderNum = 96, |
| 965 | - ParentId = system.Id, | |
| 965 | + ParentId = system.Id.ToString(), | |
| 966 | 966 | IsDeleted = false |
| 967 | 967 | }; |
| 968 | 968 | entities.Add(post); |
| ... | ... | @@ -974,7 +974,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 974 | 974 | PermissionCode = "system:post:query", |
| 975 | 975 | MenuType = MenuTypeEnum.Component, |
| 976 | 976 | OrderNum = 100, |
| 977 | - ParentId = post.Id, | |
| 977 | + ParentId = post.Id.ToString(), | |
| 978 | 978 | IsDeleted = false |
| 979 | 979 | }; |
| 980 | 980 | entities.Add(postQuery); |
| ... | ... | @@ -986,7 +986,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 986 | 986 | PermissionCode = "system:post:add", |
| 987 | 987 | MenuType = MenuTypeEnum.Component, |
| 988 | 988 | OrderNum = 100, |
| 989 | - ParentId = post.Id, | |
| 989 | + ParentId = post.Id.ToString(), | |
| 990 | 990 | IsDeleted = false |
| 991 | 991 | }; |
| 992 | 992 | entities.Add(postAdd); |
| ... | ... | @@ -998,7 +998,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 998 | 998 | PermissionCode = "system:post:edit", |
| 999 | 999 | MenuType = MenuTypeEnum.Component, |
| 1000 | 1000 | OrderNum = 100, |
| 1001 | - ParentId = post.Id, | |
| 1001 | + ParentId = post.Id.ToString(), | |
| 1002 | 1002 | IsDeleted = false |
| 1003 | 1003 | }; |
| 1004 | 1004 | entities.Add(postEdit); |
| ... | ... | @@ -1010,7 +1010,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1010 | 1010 | PermissionCode = "system:post:remove", |
| 1011 | 1011 | MenuType = MenuTypeEnum.Component, |
| 1012 | 1012 | OrderNum = 100, |
| 1013 | - ParentId = post.Id, | |
| 1013 | + ParentId = post.Id.ToString(), | |
| 1014 | 1014 | IsDeleted = false |
| 1015 | 1015 | }; |
| 1016 | 1016 | entities.Add(postRemove); |
| ... | ... | @@ -1029,7 +1029,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1029 | 1029 | Component = "system/dict/index", |
| 1030 | 1030 | MenuIcon = "dict", |
| 1031 | 1031 | OrderNum = 95, |
| 1032 | - ParentId = system.Id, | |
| 1032 | + ParentId = system.Id.ToString(), | |
| 1033 | 1033 | IsDeleted = false |
| 1034 | 1034 | }; |
| 1035 | 1035 | entities.Add(dict); |
| ... | ... | @@ -1041,7 +1041,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1041 | 1041 | PermissionCode = "system:dict:query", |
| 1042 | 1042 | MenuType = MenuTypeEnum.Component, |
| 1043 | 1043 | OrderNum = 100, |
| 1044 | - ParentId = dict.Id, | |
| 1044 | + ParentId = dict.Id.ToString(), | |
| 1045 | 1045 | IsDeleted = false |
| 1046 | 1046 | }; |
| 1047 | 1047 | entities.Add(dictQuery); |
| ... | ... | @@ -1053,7 +1053,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1053 | 1053 | PermissionCode = "system:dict:add", |
| 1054 | 1054 | MenuType = MenuTypeEnum.Component, |
| 1055 | 1055 | OrderNum = 100, |
| 1056 | - ParentId = dict.Id, | |
| 1056 | + ParentId = dict.Id.ToString(), | |
| 1057 | 1057 | IsDeleted = false |
| 1058 | 1058 | }; |
| 1059 | 1059 | entities.Add(dictAdd); |
| ... | ... | @@ -1065,7 +1065,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1065 | 1065 | PermissionCode = "system:dict:edit", |
| 1066 | 1066 | MenuType = MenuTypeEnum.Component, |
| 1067 | 1067 | OrderNum = 100, |
| 1068 | - ParentId = dict.Id, | |
| 1068 | + ParentId = dict.Id.ToString(), | |
| 1069 | 1069 | IsDeleted = false |
| 1070 | 1070 | }; |
| 1071 | 1071 | entities.Add(dictEdit); |
| ... | ... | @@ -1077,7 +1077,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1077 | 1077 | PermissionCode = "system:dict:remove", |
| 1078 | 1078 | MenuType = MenuTypeEnum.Component, |
| 1079 | 1079 | OrderNum = 100, |
| 1080 | - ParentId = dict.Id, | |
| 1080 | + ParentId = dict.Id.ToString(), | |
| 1081 | 1081 | IsDeleted = false |
| 1082 | 1082 | }; |
| 1083 | 1083 | entities.Add(dictRemove); |
| ... | ... | @@ -1097,7 +1097,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1097 | 1097 | Component = "system/config/index", |
| 1098 | 1098 | MenuIcon = "edit", |
| 1099 | 1099 | OrderNum = 94, |
| 1100 | - ParentId = system.Id, | |
| 1100 | + ParentId = system.Id.ToString(), | |
| 1101 | 1101 | IsDeleted = false |
| 1102 | 1102 | }; |
| 1103 | 1103 | entities.Add(config); |
| ... | ... | @@ -1109,7 +1109,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1109 | 1109 | PermissionCode = "system:config:query", |
| 1110 | 1110 | MenuType = MenuTypeEnum.Component, |
| 1111 | 1111 | OrderNum = 100, |
| 1112 | - ParentId = config.Id, | |
| 1112 | + ParentId = config.Id.ToString(), | |
| 1113 | 1113 | IsDeleted = false |
| 1114 | 1114 | }; |
| 1115 | 1115 | entities.Add(configQuery); |
| ... | ... | @@ -1121,7 +1121,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1121 | 1121 | PermissionCode = "system:config:add", |
| 1122 | 1122 | MenuType = MenuTypeEnum.Component, |
| 1123 | 1123 | OrderNum = 100, |
| 1124 | - ParentId = config.Id, | |
| 1124 | + ParentId = config.Id.ToString(), | |
| 1125 | 1125 | IsDeleted = false |
| 1126 | 1126 | }; |
| 1127 | 1127 | entities.Add(configAdd); |
| ... | ... | @@ -1133,7 +1133,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1133 | 1133 | PermissionCode = "system:config:edit", |
| 1134 | 1134 | MenuType = MenuTypeEnum.Component, |
| 1135 | 1135 | OrderNum = 100, |
| 1136 | - ParentId = config.Id, | |
| 1136 | + ParentId = config.Id.ToString(), | |
| 1137 | 1137 | IsDeleted = false |
| 1138 | 1138 | }; |
| 1139 | 1139 | entities.Add(configEdit); |
| ... | ... | @@ -1145,7 +1145,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1145 | 1145 | PermissionCode = "system:config:remove", |
| 1146 | 1146 | MenuType = MenuTypeEnum.Component, |
| 1147 | 1147 | OrderNum = 100, |
| 1148 | - ParentId = config.Id, | |
| 1148 | + ParentId = config.Id.ToString(), | |
| 1149 | 1149 | IsDeleted = false |
| 1150 | 1150 | }; |
| 1151 | 1151 | entities.Add(configRemove); |
| ... | ... | @@ -1167,7 +1167,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1167 | 1167 | Component = "system/notice/index", |
| 1168 | 1168 | MenuIcon = "message", |
| 1169 | 1169 | OrderNum = 93, |
| 1170 | - ParentId = system.Id, | |
| 1170 | + ParentId = system.Id.ToString(), | |
| 1171 | 1171 | IsDeleted = false |
| 1172 | 1172 | }; |
| 1173 | 1173 | entities.Add(notice); |
| ... | ... | @@ -1179,7 +1179,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1179 | 1179 | PermissionCode = "system:notice:query", |
| 1180 | 1180 | MenuType = MenuTypeEnum.Component, |
| 1181 | 1181 | OrderNum = 100, |
| 1182 | - ParentId = notice.Id, | |
| 1182 | + ParentId = notice.Id.ToString(), | |
| 1183 | 1183 | IsDeleted = false |
| 1184 | 1184 | }; |
| 1185 | 1185 | entities.Add(noticeQuery); |
| ... | ... | @@ -1191,7 +1191,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1191 | 1191 | PermissionCode = "system:notice:add", |
| 1192 | 1192 | MenuType = MenuTypeEnum.Component, |
| 1193 | 1193 | OrderNum = 100, |
| 1194 | - ParentId = notice.Id, | |
| 1194 | + ParentId = notice.Id.ToString(), | |
| 1195 | 1195 | IsDeleted = false |
| 1196 | 1196 | }; |
| 1197 | 1197 | entities.Add(noticeAdd); |
| ... | ... | @@ -1203,7 +1203,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1203 | 1203 | PermissionCode = "system:notice:edit", |
| 1204 | 1204 | MenuType = MenuTypeEnum.Component, |
| 1205 | 1205 | OrderNum = 100, |
| 1206 | - ParentId = notice.Id, | |
| 1206 | + ParentId = notice.Id.ToString(), | |
| 1207 | 1207 | IsDeleted = false |
| 1208 | 1208 | }; |
| 1209 | 1209 | entities.Add(noticeEdit); |
| ... | ... | @@ -1215,7 +1215,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1215 | 1215 | PermissionCode = "system:notice:remove", |
| 1216 | 1216 | MenuType = MenuTypeEnum.Component, |
| 1217 | 1217 | OrderNum = 100, |
| 1218 | - ParentId = notice.Id, | |
| 1218 | + ParentId = notice.Id.ToString(), | |
| 1219 | 1219 | IsDeleted = false |
| 1220 | 1220 | }; |
| 1221 | 1221 | entities.Add(noticeRemove); |
| ... | ... | @@ -1233,7 +1233,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1233 | 1233 | IsLink = false, |
| 1234 | 1234 | MenuIcon = "log", |
| 1235 | 1235 | OrderNum = 92, |
| 1236 | - ParentId = system.Id, | |
| 1236 | + ParentId = system.Id.ToString(), | |
| 1237 | 1237 | IsDeleted = false |
| 1238 | 1238 | }; |
| 1239 | 1239 | entities.Add(log); |
| ... | ... | @@ -1252,7 +1252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1252 | 1252 | Component = "monitor/operlog/index", |
| 1253 | 1253 | MenuIcon = "form", |
| 1254 | 1254 | OrderNum = 100, |
| 1255 | - ParentId = log.Id, | |
| 1255 | + ParentId = log.Id.ToString(), | |
| 1256 | 1256 | IsDeleted = false |
| 1257 | 1257 | }; |
| 1258 | 1258 | entities.Add(operationLog); |
| ... | ... | @@ -1264,7 +1264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1264 | 1264 | PermissionCode = "monitor:operlog:query", |
| 1265 | 1265 | MenuType = MenuTypeEnum.Component, |
| 1266 | 1266 | OrderNum = 100, |
| 1267 | - ParentId = operationLog.Id, | |
| 1267 | + ParentId = operationLog.Id.ToString(), | |
| 1268 | 1268 | IsDeleted = false |
| 1269 | 1269 | }; |
| 1270 | 1270 | entities.Add(operationLogQuery); |
| ... | ... | @@ -1276,7 +1276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1276 | 1276 | PermissionCode = "monitor:operlog:remove", |
| 1277 | 1277 | MenuType = MenuTypeEnum.Component, |
| 1278 | 1278 | OrderNum = 100, |
| 1279 | - ParentId = operationLog.Id, | |
| 1279 | + ParentId = operationLog.Id.ToString(), | |
| 1280 | 1280 | IsDeleted = false |
| 1281 | 1281 | }; |
| 1282 | 1282 | entities.Add(operationLogRemove); |
| ... | ... | @@ -1296,7 +1296,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1296 | 1296 | Component = "monitor/logininfor/index", |
| 1297 | 1297 | MenuIcon = "logininfor", |
| 1298 | 1298 | OrderNum = 100, |
| 1299 | - ParentId = log.Id, | |
| 1299 | + ParentId = log.Id.ToString(), | |
| 1300 | 1300 | IsDeleted = false |
| 1301 | 1301 | }; |
| 1302 | 1302 | entities.Add(loginLog); |
| ... | ... | @@ -1308,7 +1308,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1308 | 1308 | PermissionCode = "monitor:logininfor:query", |
| 1309 | 1309 | MenuType = MenuTypeEnum.Component, |
| 1310 | 1310 | OrderNum = 100, |
| 1311 | - ParentId = loginLog.Id, | |
| 1311 | + ParentId = loginLog.Id.ToString(), | |
| 1312 | 1312 | IsDeleted = false |
| 1313 | 1313 | }; |
| 1314 | 1314 | entities.Add(loginLogQuery); |
| ... | ... | @@ -1320,7 +1320,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 1320 | 1320 | PermissionCode = "monitor:logininfor:remove", |
| 1321 | 1321 | MenuType = MenuTypeEnum.Component, |
| 1322 | 1322 | OrderNum = 100, |
| 1323 | - ParentId = loginLog.Id, | |
| 1323 | + ParentId = loginLog.Id.ToString(), | |
| 1324 | 1324 | IsDeleted = false |
| 1325 | 1325 | }; |
| 1326 | 1326 | entities.Add(loginLogRemove); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuVben5DataSeed.cs
| 1 | -using Volo.Abp.Data; | |
| 1 | +using Volo.Abp.Data; | |
| 2 | 2 | using Volo.Abp.DependencyInjection; |
| 3 | 3 | using Volo.Abp.Guids; |
| 4 | 4 | using Yi.Framework.Rbac.Domain.Entities; |
| ... | ... | @@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 88 | 88 | // Component = "code/field/index", |
| 89 | 89 | // MenuIcon = "tabler:file-code", |
| 90 | 90 | // OrderNum = 99, |
| 91 | - // ParentId = code.Id, | |
| 91 | + // ParentId = code.Id.ToString(), | |
| 92 | 92 | // IsDeleted = false |
| 93 | 93 | // }; |
| 94 | 94 | // entities.Add(field); |
| ... | ... | @@ -251,7 +251,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 251 | 251 | Component = "system/tenant/index", |
| 252 | 252 | MenuIcon = "tabler:users", |
| 253 | 253 | OrderNum = 101, |
| 254 | - ParentId = system.Id, | |
| 254 | + ParentId = system.Id.ToString(), | |
| 255 | 255 | IsDeleted = false |
| 256 | 256 | }; |
| 257 | 257 | entities.Add(tenant); |
| ... | ... | @@ -263,7 +263,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 263 | 263 | PermissionCode = "system:tenant:query", |
| 264 | 264 | MenuType = MenuTypeEnum.Component, |
| 265 | 265 | OrderNum = 100, |
| 266 | - ParentId = tenant.Id, | |
| 266 | + ParentId = tenant.Id.ToString(), | |
| 267 | 267 | IsDeleted = false |
| 268 | 268 | }; |
| 269 | 269 | entities.Add(tenantQuery); |
| ... | ... | @@ -275,7 +275,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 275 | 275 | PermissionCode = "system:tenant:add", |
| 276 | 276 | MenuType = MenuTypeEnum.Component, |
| 277 | 277 | OrderNum = 100, |
| 278 | - ParentId = tenant.Id, | |
| 278 | + ParentId = tenant.Id.ToString(), | |
| 279 | 279 | IsDeleted = false |
| 280 | 280 | }; |
| 281 | 281 | entities.Add(tenantAdd); |
| ... | ... | @@ -287,7 +287,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 287 | 287 | PermissionCode = "system:tenant:edit", |
| 288 | 288 | MenuType = MenuTypeEnum.Component, |
| 289 | 289 | OrderNum = 100, |
| 290 | - ParentId = tenant.Id, | |
| 290 | + ParentId = tenant.Id.ToString(), | |
| 291 | 291 | IsDeleted = false |
| 292 | 292 | }; |
| 293 | 293 | entities.Add(tenantEdit); |
| ... | ... | @@ -299,7 +299,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 299 | 299 | PermissionCode = "system:tenant:remove", |
| 300 | 300 | MenuType = MenuTypeEnum.Component, |
| 301 | 301 | OrderNum = 100, |
| 302 | - ParentId = tenant.Id, | |
| 302 | + ParentId = tenant.Id.ToString(), | |
| 303 | 303 | IsDeleted = false |
| 304 | 304 | }; |
| 305 | 305 | entities.Add(tenantRemove); |
| ... | ... | @@ -318,7 +318,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 318 | 318 | Component = "system/user/index", |
| 319 | 319 | MenuIcon = "tabler:user", |
| 320 | 320 | OrderNum = 100, |
| 321 | - ParentId = system.Id, | |
| 321 | + ParentId = system.Id.ToString(), | |
| 322 | 322 | IsDeleted = false |
| 323 | 323 | }; |
| 324 | 324 | entities.Add(user); |
| ... | ... | @@ -330,7 +330,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 330 | 330 | PermissionCode = "system:user:query", |
| 331 | 331 | MenuType = MenuTypeEnum.Component, |
| 332 | 332 | OrderNum = 100, |
| 333 | - ParentId = user.Id, | |
| 333 | + ParentId = user.Id.ToString(), | |
| 334 | 334 | IsDeleted = false |
| 335 | 335 | }; |
| 336 | 336 | entities.Add(userQuery); |
| ... | ... | @@ -342,7 +342,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 342 | 342 | PermissionCode = "system:user:add", |
| 343 | 343 | MenuType = MenuTypeEnum.Component, |
| 344 | 344 | OrderNum = 100, |
| 345 | - ParentId = user.Id, | |
| 345 | + ParentId = user.Id.ToString(), | |
| 346 | 346 | IsDeleted = false |
| 347 | 347 | }; |
| 348 | 348 | entities.Add(userAdd); |
| ... | ... | @@ -354,7 +354,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 354 | 354 | PermissionCode = "system:user:edit", |
| 355 | 355 | MenuType = MenuTypeEnum.Component, |
| 356 | 356 | OrderNum = 100, |
| 357 | - ParentId = user.Id, | |
| 357 | + ParentId = user.Id.ToString(), | |
| 358 | 358 | IsDeleted = false |
| 359 | 359 | }; |
| 360 | 360 | entities.Add(userEdit); |
| ... | ... | @@ -366,7 +366,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 366 | 366 | PermissionCode = "system:user:remove", |
| 367 | 367 | MenuType = MenuTypeEnum.Component, |
| 368 | 368 | OrderNum = 100, |
| 369 | - ParentId = user.Id, | |
| 369 | + ParentId = user.Id.ToString(), | |
| 370 | 370 | IsDeleted = false |
| 371 | 371 | }; |
| 372 | 372 | entities.Add(userRemove); |
| ... | ... | @@ -379,7 +379,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 379 | 379 | PermissionCode = "system:user:resetPwd", |
| 380 | 380 | MenuType = MenuTypeEnum.Component, |
| 381 | 381 | OrderNum = 100, |
| 382 | - ParentId = user.Id, | |
| 382 | + ParentId = user.Id.ToString(), | |
| 383 | 383 | IsDeleted = false |
| 384 | 384 | }; |
| 385 | 385 | entities.Add(userResetPwd); |
| ... | ... | @@ -399,7 +399,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 399 | 399 | Component = "system/role/index", |
| 400 | 400 | MenuIcon = "eos-icons:role-binding-outlined", |
| 401 | 401 | OrderNum = 99, |
| 402 | - ParentId = system.Id, | |
| 402 | + ParentId = system.Id.ToString(), | |
| 403 | 403 | IsDeleted = false |
| 404 | 404 | }; |
| 405 | 405 | entities.Add(role); |
| ... | ... | @@ -411,7 +411,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 411 | 411 | PermissionCode = "system:role:query", |
| 412 | 412 | MenuType = MenuTypeEnum.Component, |
| 413 | 413 | OrderNum = 100, |
| 414 | - ParentId = role.Id, | |
| 414 | + ParentId = role.Id.ToString(), | |
| 415 | 415 | IsDeleted = false |
| 416 | 416 | }; |
| 417 | 417 | entities.Add(roleQuery); |
| ... | ... | @@ -423,7 +423,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 423 | 423 | PermissionCode = "system:role:add", |
| 424 | 424 | MenuType = MenuTypeEnum.Component, |
| 425 | 425 | OrderNum = 100, |
| 426 | - ParentId = role.Id, | |
| 426 | + ParentId = role.Id.ToString(), | |
| 427 | 427 | IsDeleted = false |
| 428 | 428 | }; |
| 429 | 429 | entities.Add(roleAdd); |
| ... | ... | @@ -435,7 +435,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 435 | 435 | PermissionCode = "system:role:edit", |
| 436 | 436 | MenuType = MenuTypeEnum.Component, |
| 437 | 437 | OrderNum = 100, |
| 438 | - ParentId = role.Id, | |
| 438 | + ParentId = role.Id.ToString(), | |
| 439 | 439 | IsDeleted = false |
| 440 | 440 | }; |
| 441 | 441 | entities.Add(roleEdit); |
| ... | ... | @@ -447,7 +447,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 447 | 447 | PermissionCode = "system:role:remove", |
| 448 | 448 | MenuType = MenuTypeEnum.Component, |
| 449 | 449 | OrderNum = 100, |
| 450 | - ParentId = role.Id, | |
| 450 | + ParentId = role.Id.ToString(), | |
| 451 | 451 | IsDeleted = false |
| 452 | 452 | }; |
| 453 | 453 | entities.Add(roleRemove); |
| ... | ... | @@ -466,7 +466,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 466 | 466 | MenuIcon = "tabler:user-shield", |
| 467 | 467 | OrderNum = 15, |
| 468 | 468 | IsDeleted = false, |
| 469 | - ParentId = system.Id | |
| 469 | + ParentId = system.Id.ToString() | |
| 470 | 470 | }; |
| 471 | 471 | entities.Add(roleAuthUser); |
| 472 | 472 | |
| ... | ... | @@ -485,7 +485,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 485 | 485 | Component = "system/menu/index", |
| 486 | 486 | MenuIcon = "ic:sharp-menu", |
| 487 | 487 | OrderNum = 98, |
| 488 | - ParentId = system.Id, | |
| 488 | + ParentId = system.Id.ToString(), | |
| 489 | 489 | IsDeleted = false |
| 490 | 490 | }; |
| 491 | 491 | entities.Add(menu); |
| ... | ... | @@ -497,7 +497,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 497 | 497 | PermissionCode = "system:menu:query", |
| 498 | 498 | MenuType = MenuTypeEnum.Component, |
| 499 | 499 | OrderNum = 100, |
| 500 | - ParentId = menu.Id, | |
| 500 | + ParentId = menu.Id.ToString(), | |
| 501 | 501 | IsDeleted = false |
| 502 | 502 | }; |
| 503 | 503 | entities.Add(menuQuery); |
| ... | ... | @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 509 | 509 | PermissionCode = "system:menu:add", |
| 510 | 510 | MenuType = MenuTypeEnum.Component, |
| 511 | 511 | OrderNum = 100, |
| 512 | - ParentId = menu.Id, | |
| 512 | + ParentId = menu.Id.ToString(), | |
| 513 | 513 | IsDeleted = false |
| 514 | 514 | }; |
| 515 | 515 | entities.Add(menuAdd); |
| ... | ... | @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 521 | 521 | PermissionCode = "system:menu:edit", |
| 522 | 522 | MenuType = MenuTypeEnum.Component, |
| 523 | 523 | OrderNum = 100, |
| 524 | - ParentId = menu.Id, | |
| 524 | + ParentId = menu.Id.ToString(), | |
| 525 | 525 | IsDeleted = false |
| 526 | 526 | }; |
| 527 | 527 | entities.Add(menuEdit); |
| ... | ... | @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 533 | 533 | PermissionCode = "system:menu:remove", |
| 534 | 534 | MenuType = MenuTypeEnum.Component, |
| 535 | 535 | OrderNum = 100, |
| 536 | - ParentId = menu.Id, | |
| 536 | + ParentId = menu.Id.ToString(), | |
| 537 | 537 | IsDeleted = false |
| 538 | 538 | }; |
| 539 | 539 | entities.Add(menuRemove); |
| ... | ... | @@ -552,7 +552,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 552 | 552 | Component = "system/dept/index", |
| 553 | 553 | MenuIcon = "mingcute:department-line", |
| 554 | 554 | OrderNum = 97, |
| 555 | - ParentId = system.Id, | |
| 555 | + ParentId = system.Id.ToString(), | |
| 556 | 556 | IsDeleted = false |
| 557 | 557 | }; |
| 558 | 558 | entities.Add(dept); |
| ... | ... | @@ -564,7 +564,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 564 | 564 | PermissionCode = "system:dept:query", |
| 565 | 565 | MenuType = MenuTypeEnum.Component, |
| 566 | 566 | OrderNum = 100, |
| 567 | - ParentId = dept.Id, | |
| 567 | + ParentId = dept.Id.ToString(), | |
| 568 | 568 | IsDeleted = false |
| 569 | 569 | }; |
| 570 | 570 | entities.Add(deptQuery); |
| ... | ... | @@ -576,7 +576,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 576 | 576 | PermissionCode = "system:dept:add", |
| 577 | 577 | MenuType = MenuTypeEnum.Component, |
| 578 | 578 | OrderNum = 100, |
| 579 | - ParentId = dept.Id, | |
| 579 | + ParentId = dept.Id.ToString(), | |
| 580 | 580 | IsDeleted = false |
| 581 | 581 | }; |
| 582 | 582 | entities.Add(deptAdd); |
| ... | ... | @@ -588,7 +588,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 588 | 588 | PermissionCode = "system:dept:edit", |
| 589 | 589 | MenuType = MenuTypeEnum.Component, |
| 590 | 590 | OrderNum = 100, |
| 591 | - ParentId = dept.Id, | |
| 591 | + ParentId = dept.Id.ToString(), | |
| 592 | 592 | IsDeleted = false |
| 593 | 593 | }; |
| 594 | 594 | entities.Add(deptEdit); |
| ... | ... | @@ -600,7 +600,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 600 | 600 | PermissionCode = "system:dept:remove", |
| 601 | 601 | MenuType = MenuTypeEnum.Component, |
| 602 | 602 | OrderNum = 100, |
| 603 | - ParentId = dept.Id, | |
| 603 | + ParentId = dept.Id.ToString(), | |
| 604 | 604 | IsDeleted = false |
| 605 | 605 | }; |
| 606 | 606 | entities.Add(deptRemove); |
| ... | ... | @@ -621,7 +621,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 621 | 621 | Component = "system/post/index", |
| 622 | 622 | MenuIcon = "tabler:user-star", |
| 623 | 623 | OrderNum = 96, |
| 624 | - ParentId = system.Id, | |
| 624 | + ParentId = system.Id.ToString(), | |
| 625 | 625 | IsDeleted = false |
| 626 | 626 | }; |
| 627 | 627 | entities.Add(post); |
| ... | ... | @@ -633,7 +633,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 633 | 633 | PermissionCode = "system:post:query", |
| 634 | 634 | MenuType = MenuTypeEnum.Component, |
| 635 | 635 | OrderNum = 100, |
| 636 | - ParentId = post.Id, | |
| 636 | + ParentId = post.Id.ToString(), | |
| 637 | 637 | IsDeleted = false |
| 638 | 638 | }; |
| 639 | 639 | entities.Add(postQuery); |
| ... | ... | @@ -645,7 +645,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 645 | 645 | PermissionCode = "system:post:add", |
| 646 | 646 | MenuType = MenuTypeEnum.Component, |
| 647 | 647 | OrderNum = 100, |
| 648 | - ParentId = post.Id, | |
| 648 | + ParentId = post.Id.ToString(), | |
| 649 | 649 | IsDeleted = false |
| 650 | 650 | }; |
| 651 | 651 | entities.Add(postAdd); |
| ... | ... | @@ -657,7 +657,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 657 | 657 | PermissionCode = "system:post:edit", |
| 658 | 658 | MenuType = MenuTypeEnum.Component, |
| 659 | 659 | OrderNum = 100, |
| 660 | - ParentId = post.Id, | |
| 660 | + ParentId = post.Id.ToString(), | |
| 661 | 661 | IsDeleted = false |
| 662 | 662 | }; |
| 663 | 663 | entities.Add(postEdit); |
| ... | ... | @@ -669,7 +669,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 669 | 669 | PermissionCode = "system:post:remove", |
| 670 | 670 | MenuType = MenuTypeEnum.Component, |
| 671 | 671 | OrderNum = 100, |
| 672 | - ParentId = post.Id, | |
| 672 | + ParentId = post.Id.ToString(), | |
| 673 | 673 | IsDeleted = false |
| 674 | 674 | }; |
| 675 | 675 | entities.Add(postRemove); |
| ... | ... | @@ -688,7 +688,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 688 | 688 | Component = "system/dict/index", |
| 689 | 689 | MenuIcon = "fluent-mdl2:dictionary", |
| 690 | 690 | OrderNum = 95, |
| 691 | - ParentId = system.Id, | |
| 691 | + ParentId = system.Id.ToString(), | |
| 692 | 692 | IsDeleted = false |
| 693 | 693 | }; |
| 694 | 694 | entities.Add(dict); |
| ... | ... | @@ -700,7 +700,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 700 | 700 | PermissionCode = "system:dict:query", |
| 701 | 701 | MenuType = MenuTypeEnum.Component, |
| 702 | 702 | OrderNum = 100, |
| 703 | - ParentId = dict.Id, | |
| 703 | + ParentId = dict.Id.ToString(), | |
| 704 | 704 | IsDeleted = false |
| 705 | 705 | }; |
| 706 | 706 | entities.Add(dictQuery); |
| ... | ... | @@ -712,7 +712,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 712 | 712 | PermissionCode = "system:dict:add", |
| 713 | 713 | MenuType = MenuTypeEnum.Component, |
| 714 | 714 | OrderNum = 100, |
| 715 | - ParentId = dict.Id, | |
| 715 | + ParentId = dict.Id.ToString(), | |
| 716 | 716 | IsDeleted = false |
| 717 | 717 | }; |
| 718 | 718 | entities.Add(dictAdd); |
| ... | ... | @@ -724,7 +724,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 724 | 724 | PermissionCode = "system:dict:edit", |
| 725 | 725 | MenuType = MenuTypeEnum.Component, |
| 726 | 726 | OrderNum = 100, |
| 727 | - ParentId = dict.Id, | |
| 727 | + ParentId = dict.Id.ToString(), | |
| 728 | 728 | IsDeleted = false |
| 729 | 729 | }; |
| 730 | 730 | entities.Add(dictEdit); |
| ... | ... | @@ -736,7 +736,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 736 | 736 | PermissionCode = "system:dict:remove", |
| 737 | 737 | MenuType = MenuTypeEnum.Component, |
| 738 | 738 | OrderNum = 100, |
| 739 | - ParentId = dict.Id, | |
| 739 | + ParentId = dict.Id.ToString(), | |
| 740 | 740 | IsDeleted = false |
| 741 | 741 | }; |
| 742 | 742 | entities.Add(dictRemove); |
| ... | ... | @@ -756,7 +756,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 756 | 756 | Component = "system/config/index", |
| 757 | 757 | MenuIcon = "ant-design:setting-outlined", |
| 758 | 758 | OrderNum = 94, |
| 759 | - ParentId = system.Id, | |
| 759 | + ParentId = system.Id.ToString(), | |
| 760 | 760 | IsDeleted = false |
| 761 | 761 | }; |
| 762 | 762 | entities.Add(config); |
| ... | ... | @@ -768,7 +768,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 768 | 768 | PermissionCode = "system:config:query", |
| 769 | 769 | MenuType = MenuTypeEnum.Component, |
| 770 | 770 | OrderNum = 100, |
| 771 | - ParentId = config.Id, | |
| 771 | + ParentId = config.Id.ToString(), | |
| 772 | 772 | IsDeleted = false |
| 773 | 773 | }; |
| 774 | 774 | entities.Add(configQuery); |
| ... | ... | @@ -780,7 +780,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 780 | 780 | PermissionCode = "system:config:add", |
| 781 | 781 | MenuType = MenuTypeEnum.Component, |
| 782 | 782 | OrderNum = 100, |
| 783 | - ParentId = config.Id, | |
| 783 | + ParentId = config.Id.ToString(), | |
| 784 | 784 | IsDeleted = false |
| 785 | 785 | }; |
| 786 | 786 | entities.Add(configAdd); |
| ... | ... | @@ -792,7 +792,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 792 | 792 | PermissionCode = "system:config:edit", |
| 793 | 793 | MenuType = MenuTypeEnum.Component, |
| 794 | 794 | OrderNum = 100, |
| 795 | - ParentId = config.Id, | |
| 795 | + ParentId = config.Id.ToString(), | |
| 796 | 796 | IsDeleted = false |
| 797 | 797 | }; |
| 798 | 798 | entities.Add(configEdit); |
| ... | ... | @@ -804,7 +804,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 804 | 804 | PermissionCode = "system:config:remove", |
| 805 | 805 | MenuType = MenuTypeEnum.Component, |
| 806 | 806 | OrderNum = 100, |
| 807 | - ParentId = config.Id, | |
| 807 | + ParentId = config.Id.ToString(), | |
| 808 | 808 | IsDeleted = false |
| 809 | 809 | }; |
| 810 | 810 | entities.Add(configRemove); |
| ... | ... | @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 826 | 826 | Component = "system/notice/index", |
| 827 | 827 | MenuIcon = "fe:notice-push", |
| 828 | 828 | OrderNum = 93, |
| 829 | - ParentId = system.Id, | |
| 829 | + ParentId = system.Id.ToString(), | |
| 830 | 830 | IsDeleted = false |
| 831 | 831 | }; |
| 832 | 832 | entities.Add(notice); |
| ... | ... | @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 838 | 838 | PermissionCode = "system:notice:query", |
| 839 | 839 | MenuType = MenuTypeEnum.Component, |
| 840 | 840 | OrderNum = 100, |
| 841 | - ParentId = notice.Id, | |
| 841 | + ParentId = notice.Id.ToString(), | |
| 842 | 842 | IsDeleted = false |
| 843 | 843 | }; |
| 844 | 844 | entities.Add(noticeQuery); |
| ... | ... | @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 850 | 850 | PermissionCode = "system:notice:add", |
| 851 | 851 | MenuType = MenuTypeEnum.Component, |
| 852 | 852 | OrderNum = 100, |
| 853 | - ParentId = notice.Id, | |
| 853 | + ParentId = notice.Id.ToString(), | |
| 854 | 854 | IsDeleted = false |
| 855 | 855 | }; |
| 856 | 856 | entities.Add(noticeAdd); |
| ... | ... | @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 862 | 862 | PermissionCode = "system:notice:edit", |
| 863 | 863 | MenuType = MenuTypeEnum.Component, |
| 864 | 864 | OrderNum = 100, |
| 865 | - ParentId = notice.Id, | |
| 865 | + ParentId = notice.Id.ToString(), | |
| 866 | 866 | IsDeleted = false |
| 867 | 867 | }; |
| 868 | 868 | entities.Add(noticeEdit); |
| ... | ... | @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 874 | 874 | PermissionCode = "system:notice:remove", |
| 875 | 875 | MenuType = MenuTypeEnum.Component, |
| 876 | 876 | OrderNum = 100, |
| 877 | - ParentId = notice.Id, | |
| 877 | + ParentId = notice.Id.ToString(), | |
| 878 | 878 | IsDeleted = false |
| 879 | 879 | }; |
| 880 | 880 | entities.Add(noticeRemove); |
| ... | ... | @@ -892,7 +892,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 892 | 892 | IsLink = false, |
| 893 | 893 | MenuIcon = "material-symbols:logo-dev-outline", |
| 894 | 894 | OrderNum = 92, |
| 895 | - ParentId = system.Id, | |
| 895 | + ParentId = system.Id.ToString(), | |
| 896 | 896 | IsDeleted = false |
| 897 | 897 | }; |
| 898 | 898 | entities.Add(log); |
| ... | ... | @@ -911,7 +911,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 911 | 911 | Component = "monitor/operlog/index", |
| 912 | 912 | MenuIcon = "tabler:align-box-right-middle", |
| 913 | 913 | OrderNum = 100, |
| 914 | - ParentId = log.Id, | |
| 914 | + ParentId = log.Id.ToString(), | |
| 915 | 915 | IsDeleted = false |
| 916 | 916 | }; |
| 917 | 917 | entities.Add(operationLog); |
| ... | ... | @@ -923,7 +923,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 923 | 923 | PermissionCode = "monitor:operlog:query", |
| 924 | 924 | MenuType = MenuTypeEnum.Component, |
| 925 | 925 | OrderNum = 100, |
| 926 | - ParentId = operationLog.Id, | |
| 926 | + ParentId = operationLog.Id.ToString(), | |
| 927 | 927 | IsDeleted = false |
| 928 | 928 | }; |
| 929 | 929 | entities.Add(operationLogQuery); |
| ... | ... | @@ -935,7 +935,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 935 | 935 | PermissionCode = "monitor:operlog:remove", |
| 936 | 936 | MenuType = MenuTypeEnum.Component, |
| 937 | 937 | OrderNum = 100, |
| 938 | - ParentId = operationLog.Id, | |
| 938 | + ParentId = operationLog.Id.ToString(), | |
| 939 | 939 | IsDeleted = false |
| 940 | 940 | }; |
| 941 | 941 | entities.Add(operationLogRemove); |
| ... | ... | @@ -955,7 +955,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 955 | 955 | Component = "monitor/logininfor/index", |
| 956 | 956 | MenuIcon = "tabler:align-box-right-middle", |
| 957 | 957 | OrderNum = 100, |
| 958 | - ParentId = log.Id, | |
| 958 | + ParentId = log.Id.ToString(), | |
| 959 | 959 | IsDeleted = false |
| 960 | 960 | }; |
| 961 | 961 | entities.Add(loginLog); |
| ... | ... | @@ -967,7 +967,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 967 | 967 | PermissionCode = "monitor:logininfor:query", |
| 968 | 968 | MenuType = MenuTypeEnum.Component, |
| 969 | 969 | OrderNum = 100, |
| 970 | - ParentId = loginLog.Id, | |
| 970 | + ParentId = loginLog.Id.ToString(), | |
| 971 | 971 | IsDeleted = false |
| 972 | 972 | }; |
| 973 | 973 | entities.Add(loginLogQuery); |
| ... | ... | @@ -979,7 +979,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds |
| 979 | 979 | PermissionCode = "monitor:logininfor:remove", |
| 980 | 980 | MenuType = MenuTypeEnum.Component, |
| 981 | 981 | OrderNum = 100, |
| 982 | - ParentId = loginLog.Id, | |
| 982 | + ParentId = loginLog.Id.ToString(), | |
| 983 | 983 | IsDeleted = false |
| 984 | 984 | }; |
| 985 | 985 | entities.Add(loginLogRemove); | ... | ... |
项目相关文档/Dashboard统计接口对接说明.md
0 → 100644
| 1 | +# Dashboard 统计接口对接说明 | |
| 2 | + | |
| 3 | +> 适用范围:美国版 Web 管理端 Dashboard 首页统计 | |
| 4 | +> | |
| 5 | +> 接口实现:`IDashboardAppService.GetOverviewAsync` / `DashboardAppService.GetOverviewAsync` | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 1. 接口信息 | |
| 10 | + | |
| 11 | +- **方法**:`GET` | |
| 12 | +- **路径**:`/api/app/dashboard/overview` | |
| 13 | +- **鉴权**:需要登录(Bearer Token) | |
| 14 | +- **请求参数**:无 | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## 2. 返回结构(顶层) | |
| 19 | + | |
| 20 | +```json | |
| 21 | +{ | |
| 22 | + "labelsPrintedToday": {}, | |
| 23 | + "activeTemplates": {}, | |
| 24 | + "activeUsers": {}, | |
| 25 | + "locations": {}, | |
| 26 | + "people": {}, | |
| 27 | + "products": {}, | |
| 28 | + "weeklyPrintVolume": [], | |
| 29 | + "byCategory": [], | |
| 30 | + "byCategoryTotal": 0, | |
| 31 | + "recentLabels": [], | |
| 32 | + "generatedAt": "2026-04-22T10:00:00+08:00", | |
| 33 | + | |
| 34 | + "metricCards": [], | |
| 35 | + "categoryDistribution": [], | |
| 36 | + "categoryDistributionTotal": 0 | |
| 37 | +} | |
| 38 | +``` | |
| 39 | + | |
| 40 | +说明: | |
| 41 | +- `labelsPrintedToday/activeTemplates/...`、`byCategory/byCategoryTotal` 是**前端直观命名**(推荐使用)。 | |
| 42 | +- `metricCards`、`categoryDistribution`、`categoryDistributionTotal` 为**兼容字段**(与旧版返回一致)。 | |
| 43 | +- `recentLabels`:**Recent Labels** 区块数据,全门店按打印时间倒序取最新 **10** 条(`fl_label_print_task`)。 | |
| 44 | + | |
| 45 | +--- | |
| 46 | + | |
| 47 | +## 3. 字段说明 | |
| 48 | + | |
| 49 | +### 3.1 指标卡片对象(`DashboardMetricCardDto`) | |
| 50 | + | |
| 51 | +用于以下字段: | |
| 52 | +- `labelsPrintedToday` | |
| 53 | +- `activeTemplates` | |
| 54 | +- `activeUsers` | |
| 55 | +- `locations` | |
| 56 | +- `people` | |
| 57 | +- `products` | |
| 58 | +- `metricCards[]`(同结构) | |
| 59 | + | |
| 60 | +| 字段 | 类型 | 说明 | | |
| 61 | +|---|---|---| | |
| 62 | +| `key` | string | 指标标识(如 `labelsPrintedToday`) | | |
| 63 | +| `title` | string | 指标标题 | | |
| 64 | +| `value` | int | 当前值 | | |
| 65 | +| `previousValue` | int | 对比周期值 | | |
| 66 | +| `changeValue` | int | 增减值(`value - previousValue`) | | |
| 67 | +| `changeRate` | decimal | 增减比例(百分比,保留 2 位) | | |
| 68 | + | |
| 69 | +--- | |
| 70 | + | |
| 71 | +### 3.2 周趋势(`weeklyPrintVolume`) | |
| 72 | + | |
| 73 | +| 字段 | 类型 | 说明 | | |
| 74 | +|---|---|---| | |
| 75 | +| `date` | string | 日期,格式 `yyyy-MM-dd` | | |
| 76 | +| `value` | int | 当天打印量 | | |
| 77 | + | |
| 78 | +--- | |
| 79 | + | |
| 80 | +### 3.3 分类分布(`byCategory`) | |
| 81 | + | |
| 82 | +`byCategory` 与 `categoryDistribution` 结构一致。 | |
| 83 | + | |
| 84 | +| 字段 | 类型 | 说明 | | |
| 85 | +|---|---|---| | |
| 86 | +| `categoryId` | string | 分类 Id(`fl_label_category.Id`) | | |
| 87 | +| `categoryName` | string | 分类名称 | | |
| 88 | +| `count` | int | 该分类下标签数量 | | |
| 89 | +| `ratio` | decimal | 占比(百分比,保留 2 位) | | |
| 90 | + | |
| 91 | +--- | |
| 92 | + | |
| 93 | +### 3.4 最近打印标签(`recentLabels`) | |
| 94 | + | |
| 95 | +数组元素类型:`DashboardRecentLabelItemDto`,按 **`PrintedAt`(`PrintedAt` 为空则用 `CreationTime`)倒序**,最多 **10** 条。 | |
| 96 | + | |
| 97 | +| 字段 | 类型 | 说明 | | |
| 98 | +|---|---|---| | |
| 99 | +| `taskId` | string | 打印任务 Id(`fl_label_print_task.Id`) | | |
| 100 | +| `labelCode` | string | 标签编码(界面 Serial,如 `1-251201`) | | |
| 101 | +| `displayName` | string | 展示标题:优先 **产品名**,否则 **标签名** | | |
| 102 | +| `printedByUserId` | string \| null | 打印人用户 Id(`CreatedBy`) | | |
| 103 | +| `printedByName` | string | 打印人展示名(`User.Name` 或 `UserName`) | | |
| 104 | +| `printedAt` | string (datetime) | 打印时间(ISO 8601) | | |
| 105 | +| `status` | string | `active` 或 `expired`:从 `PrintInputJson` 解析 `expiryDate` / `expiry` / `expirationDate`,与**当天日期**比较;无保质期或解析失败视为 `active` | | |
| 106 | +| `labelTypeBadge` | string | 模板尺寸短文案(如 `2"x2"`),用于左侧圆标 | | |
| 107 | + | |
| 108 | +前端可用 `printedAt` 自行格式化为「10 mins ago」等相对时间。 | |
| 109 | + | |
| 110 | +--- | |
| 111 | + | |
| 112 | +## 4. 返回示例 | |
| 113 | + | |
| 114 | +```json | |
| 115 | +{ | |
| 116 | + "labelsPrintedToday": { | |
| 117 | + "key": "labelsPrintedToday", | |
| 118 | + "title": "Labels Printed Today", | |
| 119 | + "value": 342, | |
| 120 | + "previousValue": 305, | |
| 121 | + "changeValue": 37, | |
| 122 | + "changeRate": 12.13 | |
| 123 | + }, | |
| 124 | + "activeTemplates": { | |
| 125 | + "key": "activeTemplates", | |
| 126 | + "title": "Active Templates", | |
| 127 | + "value": 24, | |
| 128 | + "previousValue": 22, | |
| 129 | + "changeValue": 2, | |
| 130 | + "changeRate": 9.09 | |
| 131 | + }, | |
| 132 | + "activeUsers": { | |
| 133 | + "key": "activeUsers", | |
| 134 | + "title": "Active Users", | |
| 135 | + "value": 8, | |
| 136 | + "previousValue": 7, | |
| 137 | + "changeValue": 1, | |
| 138 | + "changeRate": 14.29 | |
| 139 | + }, | |
| 140 | + "locations": { | |
| 141 | + "key": "locations", | |
| 142 | + "title": "Locations", | |
| 143 | + "value": 12, | |
| 144 | + "previousValue": 11, | |
| 145 | + "changeValue": 1, | |
| 146 | + "changeRate": 9.09 | |
| 147 | + }, | |
| 148 | + "people": { | |
| 149 | + "key": "people", | |
| 150 | + "title": "People", | |
| 151 | + "value": 48, | |
| 152 | + "previousValue": 45, | |
| 153 | + "changeValue": 3, | |
| 154 | + "changeRate": 6.67 | |
| 155 | + }, | |
| 156 | + "products": { | |
| 157 | + "key": "products", | |
| 158 | + "title": "Products", | |
| 159 | + "value": 156, | |
| 160 | + "previousValue": 156, | |
| 161 | + "changeValue": 0, | |
| 162 | + "changeRate": 0 | |
| 163 | + }, | |
| 164 | + "weeklyPrintVolume": [ | |
| 165 | + { "date": "2026-04-16", "value": 142 }, | |
| 166 | + { "date": "2026-04-17", "value": 226 }, | |
| 167 | + { "date": "2026-04-18", "value": 185 }, | |
| 168 | + { "date": "2026-04-19", "value": 261 }, | |
| 169 | + { "date": "2026-04-20", "value": 192 }, | |
| 170 | + { "date": "2026-04-21", "value": 121 }, | |
| 171 | + { "date": "2026-04-22", "value": 342 } | |
| 172 | + ], | |
| 173 | + "byCategory": [ | |
| 174 | + { "categoryId": "CAT001", "categoryName": "Breakfast", "count": 420, "ratio": 42.00 }, | |
| 175 | + { "categoryId": "CAT002", "categoryName": "Lunch", "count": 350, "ratio": 35.00 }, | |
| 176 | + { "categoryId": "CAT003", "categoryName": "Dinner", "count": 230, "ratio": 23.00 } | |
| 177 | + ], | |
| 178 | + "byCategoryTotal": 1000, | |
| 179 | + "recentLabels": [ | |
| 180 | + { | |
| 181 | + "taskId": "…", | |
| 182 | + "labelCode": "1-251201", | |
| 183 | + "displayName": "Chicken Breast", | |
| 184 | + "printedByUserId": "…", | |
| 185 | + "printedByName": "Alice J.", | |
| 186 | + "printedAt": "2026-04-22T09:50:00+08:00", | |
| 187 | + "status": "active", | |
| 188 | + "labelTypeBadge": "2\"x2\"" | |
| 189 | + } | |
| 190 | + ], | |
| 191 | + "generatedAt": "2026-04-22T10:00:00+08:00", | |
| 192 | + "metricCards": [], | |
| 193 | + "categoryDistribution": [], | |
| 194 | + "categoryDistributionTotal": 1000 | |
| 195 | +} | |
| 196 | +``` | |
| 197 | + | |
| 198 | +--- | |
| 199 | + | |
| 200 | +## 5. 统计口径说明 | |
| 201 | + | |
| 202 | +### 5.1 Labels Printed Today | |
| 203 | +- 当前值:`fl_label_print_task` 在“今日 00:00~次日 00:00”的记录数。 | |
| 204 | +- 对比值:昨日同口径记录数。 | |
| 205 | + | |
| 206 | +### 5.2 Active Templates | |
| 207 | +- 当前值:`fl_label_template` 中 `IsDeleted = false AND State = true` 数量。 | |
| 208 | +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 | |
| 209 | + | |
| 210 | +### 5.3 Active Users | |
| 211 | +- 当前值:`User` 表中 `IsDeleted = false AND State = true` 数量。 | |
| 212 | +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 | |
| 213 | + | |
| 214 | +### 5.4 Locations | |
| 215 | +- 当前值:`location` 表中 `IsDeleted = false` 数量。 | |
| 216 | +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 | |
| 217 | + | |
| 218 | +### 5.5 People | |
| 219 | +- 当前值:`User` 表中 `IsDeleted = false` 数量。 | |
| 220 | +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 | |
| 221 | + | |
| 222 | +### 5.6 Products | |
| 223 | +- 当前值:`fl_product` 表中 `IsDeleted = false` 数量。 | |
| 224 | +- 对比值:当前版本由于 `FlProductDbEntity` 未映射 `CreationTime`,临时按同口径总量返回(即变化可能为 0)。 | |
| 225 | + | |
| 226 | +### 5.7 Weekly Print Volume | |
| 227 | +- 统计最近 7 天(含今天)每天 `fl_label_print_task` 数量。 | |
| 228 | +- 无数据日期补 0。 | |
| 229 | + | |
| 230 | +### 5.8 By Category | |
| 231 | +- 基于启用且未删除的 `fl_label_category` 作为分类集合。 | |
| 232 | +- 统计 `fl_label` 中未删除且 `LabelCategoryId` 命中的数量。 | |
| 233 | +- 占比按 `count / byCategoryTotal * 100` 计算,保留 2 位。 | |
| 234 | + | |
| 235 | +--- | |
| 236 | + | |
| 237 | +## 6. 前端接入建议 | |
| 238 | + | |
| 239 | +- 新页面优先使用: | |
| 240 | + - 指标:`labelsPrintedToday`、`activeTemplates`、`activeUsers`、`locations`、`people`、`products` | |
| 241 | + - 图表:`weeklyPrintVolume` | |
| 242 | + - 环图:`byCategory` + `byCategoryTotal` | |
| 243 | +- 旧逻辑仍可使用兼容字段: | |
| 244 | + - `metricCards` | |
| 245 | + - `categoryDistribution` | |
| 246 | + - `categoryDistributionTotal` | |
| 247 | + | ... | ... |
项目相关文档/产品模块Categories接口对接说明.md
| ... | ... | @@ -8,7 +8,9 @@ |
| 8 | 8 | - **接口前缀**:宿主统一前缀为 `/api/app` |
| 9 | 9 | - **分类表**:`fl_product_category` |
| 10 | 10 | - **关联字段**:`fl_product.category_id` → `fl_product_category.id` |
| 11 | -- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) | |
| 11 | +- **外观字段(字符串落库,内容为 JSON 文本)**: | |
| 12 | + - `ButtonAppearance`(`buttonAppearance`):如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]`、或合法 JSON 对象/数组;兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时会规范为 JSON 数组,如 `["TEXT"]`)。 | |
| 13 | + - `CategoryPhotoUrl`(`categoryPhotoUrl`):与外观配合的**展示数据**,同样为 **JSON 字符串**(如 `["Prep","#10B981"]`、图片 URL 数组等);若传入**非 JSON** 的纯文本(如旧数据中的 `#EC4899` 或 `/picture/...`),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**库中字符串,由前端解析。 | |
| 12 | 14 | |
| 13 | 15 | > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 |
| 14 | 16 | |
| ... | ... | @@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi... |
| 57 | 59 | | `id` | string | 主键 | |
| 58 | 60 | | `categoryCode` | string | 类别编码 | |
| 59 | 61 | | `categoryName` | string | 类别名称 | |
| 60 | -| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) | | |
| 62 | +| `displayText` | string \| null | 按钮展示文案(空可回退 `categoryName`) | | |
| 63 | +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(含义由前端与 `buttonAppearance` 约定) | | |
| 64 | +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(见上文「外观字段」) | | |
| 65 | +| `availabilityType` | string | `ALL` / `SPECIFIED`(门店可用范围) | | |
| 61 | 66 | | `state` | boolean | 是否启用 | |
| 62 | 67 | | `orderNum` | number | 排序 | |
| 63 | 68 | | `lastEdited` | string | 最后编辑时间 | |
| ... | ... | @@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi... |
| 75 | 80 | "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", |
| 76 | 81 | "categoryCode": "CAT_PREP", |
| 77 | 82 | "categoryName": "Prep", |
| 78 | - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", | |
| 83 | + "displayText": "Prep", | |
| 84 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 85 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 86 | + "availabilityType": "ALL", | |
| 79 | 87 | "state": true, |
| 80 | 88 | "orderNum": 100, |
| 81 | 89 | "lastEdited": "2026-03-25 12:30:10" |
| ... | ... | @@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi... |
| 108 | 116 | "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", |
| 109 | 117 | "categoryCode": "CAT_PREP", |
| 110 | 118 | "categoryName": "Prep", |
| 111 | - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", | |
| 119 | + "displayText": "Prep", | |
| 120 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 121 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 122 | + "availabilityType": "ALL", | |
| 123 | + "locationIds": [], | |
| 112 | 124 | "state": true, |
| 113 | 125 | "orderNum": 100 |
| 114 | 126 | } |
| ... | ... | @@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi... |
| 130 | 142 | |------|------|------|------| |
| 131 | 143 | | `categoryCode` | string | 是 | 类别编码(唯一) | |
| 132 | 144 | | `categoryName` | string | 是 | 类别名称(唯一) | |
| 133 | -| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) | | |
| 145 | +| `displayText` | string \| null | 否 | 按钮展示文案 | | |
| 146 | +| `categoryPhotoUrl` | string \| null | 否 | **JSON 字符串**;与 `buttonAppearance` 配合(见概述)。纯路径等非 JSON 文本会被后端包成 JSON 字符串存储。 | | |
| 147 | +| `buttonAppearance` | string | 否 | **JSON 字符串**;未传或空白时后端默认 `["TEXT"]`。兼容传 `TEXT`/`COLOR`/`IMAGE` 单行(会规范为 `["TEXT"]` 等)。非法非 JSON 且非上述三者时报错。 | | |
| 148 | +| `availabilityType` | string | 否 | `ALL`(默认)或 `SPECIFIED` | | |
| 149 | +| `locationIds` | string[] | 条件 | `availabilityType=SPECIFIED` 时必填且至少 1 个门店 Id | | |
| 134 | 150 | | `state` | boolean | 否 | 是否启用(默认 true) | |
| 135 | 151 | | `orderNum` | number | 否 | 排序(默认 0) | |
| 136 | 152 | |
| ... | ... | @@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi... |
| 140 | 156 | { |
| 141 | 157 | "categoryCode": "CAT_PREP", |
| 142 | 158 | "categoryName": "Prep", |
| 143 | - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", | |
| 159 | + "displayText": "Prep", | |
| 160 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 161 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 162 | + "availabilityType": "ALL", | |
| 163 | + "locationIds": [], | |
| 144 | 164 | "state": true, |
| 145 | 165 | "orderNum": 100 |
| 146 | 166 | } |
| ... | ... | @@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi... |
| 162 | 182 | { |
| 163 | 183 | "categoryCode": "CAT_PREP", |
| 164 | 184 | "categoryName": "Prep", |
| 165 | - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", | |
| 185 | + "displayText": "Prep", | |
| 186 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 187 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 188 | + "availabilityType": "ALL", | |
| 189 | + "locationIds": [], | |
| 166 | 190 | "state": true, |
| 167 | 191 | "orderNum": 100 |
| 168 | 192 | } |
| ... | ... | @@ -179,7 +203,7 @@ Authorization: Bearer eyJhbGciOi... |
| 179 | 203 | |
| 180 | 204 | ### 约束 |
| 181 | 205 | |
| 182 | -- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。 | |
| 206 | +- 若该类别已被 `fl_product` 引用(`fl_product.CategoryId = id`),删除会失败并返回友好提示:`该类别已被产品引用,无法删除`。 | |
| 183 | 207 | |
| 184 | 208 | ### 请求示例 |
| 185 | 209 | |
| ... | ... | @@ -200,5 +224,5 @@ Authorization: Bearer eyJhbGciOi... |
| 200 | 224 | 推荐前端流程: |
| 201 | 225 | |
| 202 | 226 | 1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url` |
| 203 | -2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url` | |
| 227 | +2. 新增/编辑类别时:若采用 **JSON** 存展示数据,将 `url` 写入你方约定的 JSON 结构(例如 `["IMAGE","/picture/..."]`);若仍传**纯路径字符串**,后端会将其序列化为 JSON 字符串再入库(与仅图片场景兼容)。 | |
| 204 | 228 | ... | ... |
项目相关文档/合作伙伴Partner接口对接说明.md
0 → 100644
| 1 | +# 合作伙伴(Partner)与组织(Group)接口对接说明 | |
| 2 | + | |
| 3 | +> 适用范围:美国版 Web 管理端「Account Management」下的 **Partner**、**Group** 主数据 | |
| 4 | +> **Partner** 表:`fl_partner`,接口:`IPartnerAppService` / `PartnerAppService` | |
| 5 | +> **Group** 表:`fl_group`(`PartnerId` 关联 `fl_partner.Id`),接口:`IGroupAppService` / `GroupAppService` | |
| 6 | +> 宿主路由前缀:`/api/app`(与 `YiAbpWebModule` 中 `RootPath = api/app` 一致) | |
| 7 | + | |
| 8 | +--- | |
| 9 | + | |
| 10 | +## 0. 通用说明 | |
| 11 | + | |
| 12 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`),与其它 `/api/app/*` 接口一致。 | |
| 13 | +- **Content-Type**:`POST` / `PUT` 使用 `application/json`。 | |
| 14 | +- **分页约定(美国版食品标签模块)**:`skipCount` 表示**页码(从 1 起)**,不是 0 基 offset;第一页请传 `skipCount=1`。与 `PagedQueryConvention` 及 SqlSugar `ToPageListAsync` 用法一致。 | |
| 15 | +- **逻辑删除**:`DELETE` 将对应表的 `IsDeleted` 置为 `true`(`fl_partner` / `fl_group`),不物理删行。 | |
| 16 | +- **列表与导出筛选一致**:各模块列表的筛选字段与对应 **export-pdf** 接口一致,便于数据对齐。 | |
| 17 | +- **Group 与 Partner**:新建/编辑 Group 时 `partnerId` 必须指向**未逻辑删除**的 `fl_partner`;列表左联 Partner 时仅展示未删除合作伙伴名称,已删 Partner 在列表中显示为 **`无`**。 | |
| 18 | + | |
| 19 | +--- | |
| 20 | + | |
| 21 | +# 第一部分:Partner(合作伙伴) | |
| 22 | + | |
| 23 | +## 1. 分页列表 | |
| 24 | + | |
| 25 | +- **方法**:`GET` | |
| 26 | +- **路径**:`/api/app/partner` | |
| 27 | + | |
| 28 | +### 1.1 查询参数(`PartnerGetListInputVo`) | |
| 29 | + | |
| 30 | +| 参数 | 类型 | 必填 | 说明 | | |
| 31 | +|------|------|------|------| | |
| 32 | +| `skipCount` | int | 是 | 页码,从 **1** 开始 | | |
| 33 | +| `maxResultCount` | int | 是 | 每页条数 | | |
| 34 | +| `sorting` | string | 否 | 排序,仅支持白名单(见下) | | |
| 35 | +| `keyword` | string | 否 | 模糊匹配 `PartnerName`、`ContactEmail`、`PhoneNumber` | | |
| 36 | +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 | | |
| 37 | + | |
| 38 | +**排序白名单**(大小写不敏感): | |
| 39 | + | |
| 40 | +- `PartnerName asc` / `PartnerName desc` | |
| 41 | +- `CreationTime asc` / `CreationTime desc` | |
| 42 | +- `State asc` / `State desc` | |
| 43 | + | |
| 44 | +其它值将回退为默认:**按 `CreationTime` 降序**。 | |
| 45 | + | |
| 46 | +### 1.2 请求示例 | |
| 47 | + | |
| 48 | +```http | |
| 49 | +GET /api/app/partner?skipCount=1&maxResultCount=10&keyword=Global&state=true&sorting=CreationTime%20desc HTTP/1.1 | |
| 50 | +Authorization: Bearer {token} | |
| 51 | +``` | |
| 52 | + | |
| 53 | +### 1.3 响应结构(`PagedResultWithPageDto<PartnerGetListOutputDto>`) | |
| 54 | + | |
| 55 | +```json | |
| 56 | +{ | |
| 57 | + "pageIndex": 1, | |
| 58 | + "pageSize": 10, | |
| 59 | + "totalCount": 2, | |
| 60 | + "totalPages": 1, | |
| 61 | + "items": [ | |
| 62 | + { | |
| 63 | + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", | |
| 64 | + "partnerName": "Global Foods Inc.", | |
| 65 | + "contactEmail": "admin@globalfoods.com", | |
| 66 | + "phoneNumber": "+1 (555) 100-2000", | |
| 67 | + "state": true, | |
| 68 | + "creationTime": "2026-04-27T10:00:00" | |
| 69 | + } | |
| 70 | + ] | |
| 71 | +} | |
| 72 | +``` | |
| 73 | + | |
| 74 | +### 1.4 列表项字段(`PartnerGetListOutputDto`) | |
| 75 | + | |
| 76 | +| 字段 | 类型 | 说明 | | |
| 77 | +|------|------|------| | |
| 78 | +| `id` | string | 主键 | | |
| 79 | +| `partnerName` | string | 合作伙伴名称 | | |
| 80 | +| `contactEmail` | string \| null | 联系邮箱 | | |
| 81 | +| `phoneNumber` | string \| null | 电话 | | |
| 82 | +| `state` | bool | 是否启用(UI Active 开关) | | |
| 83 | +| `creationTime` | string (datetime) | 创建时间 | | |
| 84 | + | |
| 85 | +--- | |
| 86 | + | |
| 87 | +## 2. 详情 | |
| 88 | + | |
| 89 | +- **方法**:`GET` | |
| 90 | +- **路径**:`/api/app/partner/{id}` | |
| 91 | + | |
| 92 | +### 2.1 路径参数 | |
| 93 | + | |
| 94 | +| 参数 | 说明 | | |
| 95 | +|------|------| | |
| 96 | +| `id` | `fl_partner.Id` | | |
| 97 | + | |
| 98 | +### 2.2 响应结构(`PartnerGetOutputDto`) | |
| 99 | + | |
| 100 | +| 字段 | 类型 | 说明 | | |
| 101 | +|------|------|------| | |
| 102 | +| `id` | string | 主键 | | |
| 103 | +| `partnerName` | string | 合作伙伴名称 | | |
| 104 | +| `contactEmail` | string \| null | 联系邮箱 | | |
| 105 | +| `phoneNumber` | string \| null | 电话 | | |
| 106 | +| `state` | bool | 是否启用 | | |
| 107 | +| `creationTime` | string (datetime) | 创建时间 | | |
| 108 | +| `lastModificationTime` | string (datetime) \| null | 最后修改时间 | | |
| 109 | + | |
| 110 | +### 2.3 错误说明 | |
| 111 | + | |
| 112 | +- `id` 为空或记录不存在(含已逻辑删除):业务错误提示「合作伙伴不存在」等。 | |
| 113 | + | |
| 114 | +--- | |
| 115 | + | |
| 116 | +## 3. 新增 | |
| 117 | + | |
| 118 | +- **方法**:`POST` | |
| 119 | +- **路径**:`/api/app/partner` | |
| 120 | + | |
| 121 | +### 3.1 Body(`PartnerCreateInputVo`) | |
| 122 | + | |
| 123 | +```json | |
| 124 | +{ | |
| 125 | + "partnerName": "Global Foods Inc.", | |
| 126 | + "contactEmail": "admin@globalfoods.com", | |
| 127 | + "phoneNumber": "+1 (555) 100-2000", | |
| 128 | + "state": true | |
| 129 | +} | |
| 130 | +``` | |
| 131 | + | |
| 132 | +| 字段 | 类型 | 必填 | 说明 | | |
| 133 | +|------|------|------|------| | |
| 134 | +| `partnerName` | string | 是 | 合作伙伴名称,去首尾空格后不能为空 | | |
| 135 | +| `contactEmail` | string | 否 | 若填写则做简单格式校验(含 `@` 等) | | |
| 136 | +| `phoneNumber` | string | 否 | 电话 | | |
| 137 | +| `state` | bool | 否 | 默认 `true` | | |
| 138 | + | |
| 139 | +### 3.2 响应 | |
| 140 | + | |
| 141 | +- 成功:返回 `PartnerGetOutputDto`(与详情结构一致)。 | |
| 142 | + | |
| 143 | +--- | |
| 144 | + | |
| 145 | +## 4. 编辑 | |
| 146 | + | |
| 147 | +- **方法**:`PUT` | |
| 148 | +- **路径**:`/api/app/partner/{id}` | |
| 149 | + | |
| 150 | +### 4.1 参数 | |
| 151 | + | |
| 152 | +- **Path**:`id` 为当前合作伙伴主键。 | |
| 153 | +- **Body**:`PartnerUpdateInputVo`,字段与新增相同。 | |
| 154 | + | |
| 155 | +```json | |
| 156 | +{ | |
| 157 | + "partnerName": "Global Foods Inc.", | |
| 158 | + "contactEmail": "admin@globalfoods.com", | |
| 159 | + "phoneNumber": "+1 (555) 100-2000", | |
| 160 | + "state": false | |
| 161 | +} | |
| 162 | +``` | |
| 163 | + | |
| 164 | +### 4.2 响应 | |
| 165 | + | |
| 166 | +- 成功:返回更新后的 `PartnerGetOutputDto`。 | |
| 167 | + | |
| 168 | +--- | |
| 169 | + | |
| 170 | +## 5. 删除(逻辑删除) | |
| 171 | + | |
| 172 | +- **方法**:`DELETE` | |
| 173 | +- **路径**:`/api/app/partner/{id}` | |
| 174 | + | |
| 175 | +### 5.1 路径参数 | |
| 176 | + | |
| 177 | +| 参数 | 说明 | | |
| 178 | +|------|------| | |
| 179 | +| `id` | `fl_partner.Id` | | |
| 180 | + | |
| 181 | +### 5.2 行为 | |
| 182 | + | |
| 183 | +- 将 `IsDeleted` 置为 `true`,并更新 `LastModificationTime` / `LastModifierId`(若当前用户存在)。 | |
| 184 | + | |
| 185 | +--- | |
| 186 | + | |
| 187 | +## 6. 批量导出 PDF | |
| 188 | + | |
| 189 | +- **方法**:`GET` | |
| 190 | +- **路径**:`/api/app/partner/export-pdf` | |
| 191 | +- **响应**:`Content-Type: application/pdf`,附件名形如 `partners_yyyy-MM-dd_HH-mm-ss.pdf` | |
| 192 | + | |
| 193 | +### 6.1 查询参数 | |
| 194 | + | |
| 195 | +与列表接口相同的筛选字段(分页字段可忽略): | |
| 196 | + | |
| 197 | +| 参数 | 类型 | 必填 | 说明 | | |
| 198 | +|------|------|------|------| | |
| 199 | +| `keyword` | string | 否 | 与列表 `keyword` 一致 | | |
| 200 | +| `state` | bool | 否 | 与列表 `state` 一致 | | |
| 201 | +| `sorting` | string | 否 | 与列表白名单一致,用于导出行顺序 | | |
| 202 | + | |
| 203 | +### 6.2 限制 | |
| 204 | + | |
| 205 | +- 命中行数 **超过 5000** 时接口返回业务错误,需缩小筛选范围后再导出。 | |
| 206 | +- 导出最多取 **5000** 条,排序与列表查询逻辑一致。 | |
| 207 | + | |
| 208 | +### 6.3 请求示例 | |
| 209 | + | |
| 210 | +```http | |
| 211 | +GET /api/app/partner/export-pdf?keyword=Global&state=true HTTP/1.1 | |
| 212 | +Authorization: Bearer {token} | |
| 213 | +``` | |
| 214 | + | |
| 215 | +### 6.4 PDF 内容说明 | |
| 216 | + | |
| 217 | +- 表头列:**Partner**、**Contact**、**Phone**、**Status**、**Created**。 | |
| 218 | +- `Status` 文本:`state === true` 时为 `active`,否则 `inactive`。 | |
| 219 | +- 空邮箱、空电话在 PDF 中显示为 **`无`**(与项目列表空值展示约定一致)。 | |
| 220 | + | |
| 221 | +--- | |
| 222 | + | |
| 223 | +## 7. 数据库与建表(Partner) | |
| 224 | + | |
| 225 | +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql` | |
| 226 | +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`PartnerName`、`ContactEmail`、`PhoneNumber`、`State` | |
| 227 | + | |
| 228 | +--- | |
| 229 | + | |
| 230 | +## 8. 与门店字段的关系说明 | |
| 231 | + | |
| 232 | +- 门店(`location`)上可能存在 **`Partner` 字符串字段**(原型/筛选用),与本文 **`fl_partner` 主数据表** 无强制外键关联。 | |
| 233 | +- 若后续要将门店关联到合作伙伴主数据,需单独产品方案(例如增加 `PartnerId` 或同步名称)。 | |
| 234 | + | |
| 235 | +--- | |
| 236 | + | |
| 237 | +## 9. 前端对接提示(Partner) | |
| 238 | + | |
| 239 | +- 列表「Search」对应 `keyword`;「Active」筛选对应 `state`。 | |
| 240 | +- 「Bulk Export (PDF)」调用 **第 6 节** 导出接口,查询参数与当前列表筛选保持一致即可。 | |
| 241 | + | |
| 242 | +--- | |
| 243 | + | |
| 244 | +# 第二部分:Group(组织 / Group) | |
| 245 | + | |
| 246 | +> UI:**Group Name**、**Parent Partner**(下拉绑定合作伙伴)、**Status**、**Bulk Export (PDF)**、**New+** 弹窗(Group Name、Assign to Partner、Active)。 | |
| 247 | + | |
| 248 | +## 10. 数据库与建表(Group) | |
| 249 | + | |
| 250 | +- 库中原先**无**独立 Group 业务表;新建表名:`fl_group`。 | |
| 251 | +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql` | |
| 252 | +- **须先存在 `fl_partner` 表**(脚本内含外键 `FK_fl_group_partner` → `fl_partner(Id)`)。 | |
| 253 | +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`GroupName`、`PartnerId`、`State` | |
| 254 | + | |
| 255 | +**建表 SQL(与脚本文件一致,便于直接执行):** | |
| 256 | + | |
| 257 | +```sql | |
| 258 | +CREATE TABLE IF NOT EXISTS `fl_group` ( | |
| 259 | + `Id` varchar(64) NOT NULL COMMENT '主键', | |
| 260 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 261 | + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', | |
| 262 | + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', | |
| 263 | + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', | |
| 264 | + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', | |
| 265 | + `GroupName` varchar(256) NOT NULL COMMENT '组织名称', | |
| 266 | + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id', | |
| 267 | + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | |
| 268 | + PRIMARY KEY (`Id`), | |
| 269 | + KEY `IX_fl_group_IsDeleted` (`IsDeleted`), | |
| 270 | + KEY `IX_fl_group_State` (`State`), | |
| 271 | + KEY `IX_fl_group_PartnerId` (`PartnerId`), | |
| 272 | + KEY `IX_fl_group_GroupName` (`GroupName`(128)), | |
| 273 | + KEY `IX_fl_group_CreationTime` (`CreationTime`), | |
| 274 | + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`) | |
| 275 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)'; | |
| 276 | +``` | |
| 277 | + | |
| 278 | +--- | |
| 279 | + | |
| 280 | +## 11. Group — 分页列表 | |
| 281 | + | |
| 282 | +- **方法**:`GET` | |
| 283 | +- **路径**:`/api/app/group` | |
| 284 | + | |
| 285 | +### 11.1 查询参数(`GroupGetListInputVo`) | |
| 286 | + | |
| 287 | +| 参数 | 类型 | 必填 | 说明 | | |
| 288 | +|------|------|------|------| | |
| 289 | +| `skipCount` | int | 是 | 页码,从 **1** 开始 | | |
| 290 | +| `maxResultCount` | int | 是 | 每页条数 | | |
| 291 | +| `sorting` | string | 否 | 排序白名单(见下) | | |
| 292 | +| `keyword` | string | 否 | 模糊匹配 `GroupName`、所属 **未删除** Partner 的 `PartnerName` | | |
| 293 | +| `partnerId` | string | 否 | 仅查看某合作伙伴下的组织(`fl_partner.Id`) | | |
| 294 | +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 | | |
| 295 | + | |
| 296 | +**排序白名单**(大小写不敏感): | |
| 297 | + | |
| 298 | +- `GroupName asc` / `GroupName desc` | |
| 299 | +- `CreationTime asc` / `CreationTime desc` | |
| 300 | +- `State asc` / `State desc` | |
| 301 | +- `PartnerName asc` / `PartnerName desc`(按关联合作伙伴名称) | |
| 302 | + | |
| 303 | +其它值回退为默认:**按 `CreationTime` 降序**。 | |
| 304 | + | |
| 305 | +### 11.2 请求示例 | |
| 306 | + | |
| 307 | +```http | |
| 308 | +GET /api/app/group?skipCount=1&maxResultCount=10&keyword=West&partnerId={partnerGuid}&state=true HTTP/1.1 | |
| 309 | +Authorization: Bearer {token} | |
| 310 | +``` | |
| 311 | + | |
| 312 | +### 11.3 响应结构(`PagedResultWithPageDto<GroupGetListOutputDto>`) | |
| 313 | + | |
| 314 | +```json | |
| 315 | +{ | |
| 316 | + "pageIndex": 1, | |
| 317 | + "pageSize": 10, | |
| 318 | + "totalCount": 2, | |
| 319 | + "totalPages": 1, | |
| 320 | + "items": [ | |
| 321 | + { | |
| 322 | + "id": "…", | |
| 323 | + "groupName": "West Coast Region", | |
| 324 | + "partnerId": "…", | |
| 325 | + "partnerName": "Global Foods Inc.", | |
| 326 | + "state": true, | |
| 327 | + "creationTime": "2026-04-27T10:00:00" | |
| 328 | + } | |
| 329 | + ] | |
| 330 | +} | |
| 331 | +``` | |
| 332 | + | |
| 333 | +### 11.4 列表项字段 | |
| 334 | + | |
| 335 | +| 字段 | 类型 | 说明 | | |
| 336 | +|------|------|------| | |
| 337 | +| `id` | string | 主键 | | |
| 338 | +| `groupName` | string | 组织名称 | | |
| 339 | +| `partnerId` | string | 所属合作伙伴 Id | | |
| 340 | +| `partnerName` | string | 父级合作伙伴名称(UI「Parent Partner」) | | |
| 341 | +| `state` | bool | 是否启用 | | |
| 342 | +| `creationTime` | string (datetime) | 创建时间 | | |
| 343 | + | |
| 344 | +--- | |
| 345 | + | |
| 346 | +## 12. Group — 详情 | |
| 347 | + | |
| 348 | +- **方法**:`GET` | |
| 349 | +- **路径**:`/api/app/group/{id}` | |
| 350 | + | |
| 351 | +路径参数 `id`:`fl_group.Id`。响应为 `GroupGetOutputDto`(在列表字段基础上增加 `lastModificationTime`)。 | |
| 352 | + | |
| 353 | +--- | |
| 354 | + | |
| 355 | +## 13. Group — 新增 | |
| 356 | + | |
| 357 | +- **方法**:`POST` | |
| 358 | +- **路径**:`/api/app/group` | |
| 359 | + | |
| 360 | +### Body(`GroupCreateInputVo`) | |
| 361 | + | |
| 362 | +```json | |
| 363 | +{ | |
| 364 | + "groupName": "West Coast Region", | |
| 365 | + "partnerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", | |
| 366 | + "state": true | |
| 367 | +} | |
| 368 | +``` | |
| 369 | + | |
| 370 | +| 字段 | 类型 | 必填 | 说明 | | |
| 371 | +|------|------|------|------| | |
| 372 | +| `groupName` | string | 是 | 组织名称 | | |
| 373 | +| `partnerId` | string | 是 | Assign to Partner,须为未删除的 `fl_partner.Id` | | |
| 374 | +| `state` | bool | 否 | 默认 `true` | | |
| 375 | + | |
| 376 | +--- | |
| 377 | + | |
| 378 | +## 14. Group — 编辑 | |
| 379 | + | |
| 380 | +- **方法**:`PUT` | |
| 381 | +- **路径**:`/api/app/group/{id}` | |
| 382 | +- **Body**:`GroupUpdateInputVo`(字段同新增) | |
| 383 | + | |
| 384 | +--- | |
| 385 | + | |
| 386 | +## 15. Group — 删除(逻辑删除) | |
| 387 | + | |
| 388 | +- **方法**:`DELETE` | |
| 389 | +- **路径**:`/api/app/group/{id}` | |
| 390 | + | |
| 391 | +--- | |
| 392 | + | |
| 393 | +## 16. Group — 批量导出 PDF | |
| 394 | + | |
| 395 | +- **方法**:`GET` | |
| 396 | +- **路径**:`/api/app/group/export-pdf` | |
| 397 | +- **响应**:`application/pdf`,附件名形如 `groups_yyyy-MM-dd_HH-mm-ss.pdf` | |
| 398 | + | |
| 399 | +### 16.1 查询参数 | |
| 400 | + | |
| 401 | +与列表一致(分页可忽略):`keyword`、`partnerId`、`state`、`sorting`。 | |
| 402 | + | |
| 403 | +### 16.2 限制 | |
| 404 | + | |
| 405 | +- 命中行数 **超过 5000** 返回业务错误;导出最多 **5000** 条。 | |
| 406 | + | |
| 407 | +### 16.3 PDF 列 | |
| 408 | + | |
| 409 | +**Group Name**、**Parent Partner**、**Status**(`active` / `inactive`)、**Created**。 | |
| 410 | + | |
| 411 | +--- | |
| 412 | + | |
| 413 | +## 17. 前端对接提示(Group) | |
| 414 | + | |
| 415 | +- 「Search」→ `keyword`;按父级合作伙伴筛选 → `partnerId`(下拉选中项的 `id`);「Active」→ `state`。 | |
| 416 | +- 「Assign to Partner」下拉数据来自 **Partner 列表接口**(`/api/app/partner`)。 | |
| 417 | +- 「Bulk Export (PDF)」→ **第 16 节**,查询参数与当前列表筛选一致。 | ... | ... |
项目相关文档/平台端Categories图片上传接口说明.md
| ... | ... | @@ -54,7 +54,7 @@ curl -X POST "http://localhost:19001/api/app/picture/category/upload" ^ |
| 54 | 54 | |
| 55 | 55 | | 字段 | 类型 | 说明 | |
| 56 | 56 | |------|------|------| |
| 57 | -| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) | | |
| 57 | +| `url` | string | 图片访问的相对路径;写入分类接口时,若 `categoryPhotoUrl` 采用 **JSON** 存展示数据,请将该 `url` 放入你方约定的 JSON 结构中。若仍传**纯路径字符串**,后端会序列化为 JSON 字符串再入库。 | | |
| 58 | 58 | | `fileName` | string | 服务器保存的文件名 | |
| 59 | 59 | | `size` | number | 文件大小(字节) | |
| 60 | 60 | |
| ... | ... | @@ -96,7 +96,7 @@ curl -X POST "http://localhost:19001/api/app/picture/category/upload" ^ |
| 96 | 96 | 推荐前端流程: |
| 97 | 97 | |
| 98 | 98 | 1. 调用本上传接口,拿到返回的 `url` |
| 99 | -2. 再调用分类新增/编辑接口,把 `categoryPhotoUrl` 设置为该 `url` | |
| 99 | +2. 再调用分类新增/编辑接口:按平台与 **`buttonAppearance`(JSON 字符串)** 的约定组装 `categoryPhotoUrl`(JSON);或继续传纯 `url` 由后端自动包成 JSON 字符串。 | |
| 100 | 100 | |
| 101 | -> 说明:分类 CRUD 已支持 `CategoryPhotoUrl` 字段;你只需要在页面表单里新增该字段即可。 | |
| 101 | +> 说明:详见 `项目相关文档/产品模块Categories接口对接说明.md`、`项目相关文档/标签模块接口对接说明.md` 中「JSON 字符串」约定。 | |
| 102 | 102 | ... | ... |
项目相关文档/报表Reports接口对接说明.md
0 → 100644
| 1 | +# 报表 Reports 接口对接说明(Print Log / Label Report) | |
| 2 | + | |
| 3 | +> 适用范围:美国版 Web「Reports」— **Print Log** 列表与导出、**Label Report** 统计与导出、**重打** | |
| 4 | +> 实现:`IReportsAppService` / `ReportsAppService`;重打复用 `IUsAppLabelingAppService.ReprintAsync`(已支持 admin 跳过创建人校验) | |
| 5 | +> 路由前缀:`/api/app` | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 0. 角色与数据范围(必读) | |
| 10 | + | |
| 11 | +- 判断依据:`CurrentUser.Roles` 中是否存在**忽略大小写**等于 **`admin`** 的角色码(与 JWT 中角色码一致,参见 `AuthSessionAppService` / `ReportsRoleHelper`)。 | |
| 12 | +- **`admin`**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 | |
| 13 | +- **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。 | |
| 14 | +- **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。 | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## 1. Partner / Group / Location 筛选说明 | |
| 19 | + | |
| 20 | +- **`locationId`**:若传则只查该门店(`location.Id` 字符串)。 | |
| 21 | +- **`partnerId`**(`fl_partner.Id`):按合作伙伴名称与 `location.Partner` **文本全等**(trim 后)匹配,得到门店集合再过滤任务。 | |
| 22 | +- **`groupId`**(`fl_group.Id`):按组织的 `GroupName` + 父级 `PartnerName` 与门店的 `GroupName` + `Partner` **文本全等**匹配门店。 | |
| 23 | +- 若 Partner/Group 在库中不存在,返回**空列表/空统计**(不报错)。 | |
| 24 | +- 未传 Partner/Group/Location 时:**不按门店集合预过滤**(仅日期、关键字、用户范围生效)。 | |
| 25 | + | |
| 26 | +--- | |
| 27 | + | |
| 28 | +## 2. Print Log — 分页查询 | |
| 29 | + | |
| 30 | +- **方法**:`GET` | |
| 31 | +- **路径**:`/api/app/reports/print-log-list`(以 Swagger 为准;ABP 约定多为 `get-print-log-list` 映射到该路径) | |
| 32 | + | |
| 33 | +### 2.1 查询参数(`ReportsPrintLogGetListInputVo`) | |
| 34 | + | |
| 35 | +| 参数 | 类型 | 说明 | | |
| 36 | +|------|------|------| | |
| 37 | +| `skipCount` | int | 页码,从 **1** 起 | | |
| 38 | +| `maxResultCount` | int | 每页条数 | | |
| 39 | +| `sorting` | string | 可选:`PrintedAt asc` / `PrintedAt desc`(默认倒序) | | |
| 40 | +| `partnerId` | string | 可选 | | |
| 41 | +| `groupId` | string | 可选 | | |
| 42 | +| `locationId` | string | 可选 | | |
| 43 | +| `startDate` | date | 可选;默认与结束日组成约 30 天窗口 | | |
| 44 | +| `endDate` | date | 可选;默认今天(含当日) | | |
| 45 | +| `keyword` | string | 可选;匹配产品名、标签分类名、产品分类名(模糊) | | |
| 46 | + | |
| 47 | +### 2.2 响应项(`ReportsPrintLogListItemDto`) | |
| 48 | + | |
| 49 | +含:`taskId`、`labelCode`、`productName`、`categoryName`、`templateText`、`printedAt`、`printedByName`、`locationText`、`locationId`(重打必填)、`expiryDateText`(从 `PrintInputJson` 中尝试解析 `expiryDate` / `expiry` / `expirationDate`)。 | |
| 50 | + | |
| 51 | +--- | |
| 52 | + | |
| 53 | +## 3. Print Log — 导出 PDF | |
| 54 | + | |
| 55 | +- **方法**:`GET` | |
| 56 | +- **路径**:`/api/app/reports/export-print-log-pdf` | |
| 57 | +- **查询参数**:与 **§2** 相同(分页字段忽略);最多 **5000** 条,超出返回业务错误。 | |
| 58 | + | |
| 59 | +--- | |
| 60 | + | |
| 61 | +## 4. Print Log — 重打(Reprint) | |
| 62 | + | |
| 63 | +- **方法**:`POST` | |
| 64 | +- **路径**:`/api/app/reports/reprint-print-log`(实现内转发至 `UsAppLabelingAppService.ReprintAsync`,以 Swagger 为准) | |
| 65 | +- **Body**:`UsAppLabelReprintInputVo`:`locationId`、`taskId`、`printQuantity`、`clientRequestId`(可选)、打印机字段(可选)。 | |
| 66 | + | |
| 67 | +--- | |
| 68 | + | |
| 69 | +## 5. Label Report — 统计聚合 | |
| 70 | + | |
| 71 | +- **方法**:`GET` | |
| 72 | +- **路径**:`/api/app/reports/label-report`(以 Swagger 为准) | |
| 73 | + | |
| 74 | +### 5.1 查询参数(`ReportsLabelReportQueryInputVo`) | |
| 75 | + | |
| 76 | +与 Print Log 相同的 `partnerId`、`groupId`、`locationId`、`startDate`、`endDate`、`keyword`(无分页)。 | |
| 77 | + | |
| 78 | +### 5.2 默认时间窗 | |
| 79 | + | |
| 80 | +未传日期时:**结束日 = 今天,开始日 = 结束日前推 29 天(共约 30 个自然日,含首尾)**。 | |
| 81 | + | |
| 82 | +### 5.3 返回(`ReportsLabelReportOutputDto`) | |
| 83 | + | |
| 84 | +- **`summary`**:`totalLabelsPrinted`、上一同长周期 `totalLabelsPrintedPrevPeriod`、`totalLabelsPrintedChangeRate`(%);最热门标签分类名与次数;Top 产品名与次数;`avgDailyPrints` 及环比等。 | |
| 85 | +- **`labelsByCategory`**:按 **标签分类**(`fl_label_category`)汇总当前区间内打印次数。 | |
| 86 | +- **`printVolumeTrend`**:在**当前筛选日期区间内**,取**结束日向前最多 7 个自然日**(与区间求交)的按日打印量。 | |
| 87 | +- **`mostUsedProducts`**:当前区间内产品打印次数 Top20,`usagePercent` 为占**当期总打印次数**的百分比。 | |
| 88 | + | |
| 89 | +--- | |
| 90 | + | |
| 91 | +## 6. Label Report — 导出 PDF | |
| 92 | + | |
| 93 | +- **方法**:`GET` | |
| 94 | +- **路径**:`/api/app/reports/export-label-report-pdf` | |
| 95 | +- **查询参数**:与 **§5** 相同;内容为摘要 + 分类表 + 日趋势表 + Top 产品表。 | |
| 96 | + | |
| 97 | +--- | |
| 98 | + | |
| 99 | +## 7. 数据表与字段依赖 | |
| 100 | + | |
| 101 | +- 打印任务:`fl_label_print_task`(`CreatedBy`、`LocationId`、`PrintedAt`、`PrintInputJson` 等) | |
| 102 | +- 门店:`location`(`Partner`、`GroupName`、`LocationCode`、`LocationName`) | |
| 103 | +- 主数据:`fl_partner`、`fl_group`(筛选用) | |
| 104 | +- 用户:`User`(展示 `PrintedByName`) | |
| 105 | + | |
| 106 | +--- | |
| 107 | + | |
| 108 | +## 8. 前端对接提示 | |
| 109 | + | |
| 110 | +- Print Log 行内 **Reprint**:使用列表返回的 `locationId` + `taskId` 调重打接口。 | |
| 111 | +- 「Export Report」在 Print Log Tab 调 **§3**;在 Label Report Tab 调 **§6**。 | |
| 112 | +- 下拉 **Partner / Group / Location** 与列表筛选字段一致;需保证门店维护的 `Partner` / `GroupName` 与主数据名称一致,否则筛选结果为空。 | ... | ... |
项目相关文档/本次新增与优化接口汇总.md
0 → 100644
| 1 | +# 本次接口变更汇总(标签 / 产品类别 / 模板组件 / App 树 / Web 会话) | |
| 2 | + | |
| 3 | +> 说明:本文只汇总本次迭代中相关内容: | |
| 4 | +> - 标签模块 Label Categories:对齐“新增类别”原型图(`buttonAppearance` + `categoryPhotoUrl` / 展示文案 / 门店范围) | |
| 5 | +> - 产品模块 Categories:对齐“新增产品类别”原型图(同上) | |
| 6 | +> - 模板组件(`fl_label_template_element`):新增字段 `TypeAdd`(并补齐 `ElementName`) | |
| 7 | +> - App `labeling-tree`:L1 标签分类返回 `buttonAppearance` | |
| 8 | +> - Web 管理端 `auth-session`:**当前用户菜单与权限**、**退出登录**(食品标签-美国版模块) | |
| 9 | +> - Web `rbac-menu` 列表/详情:补充返回 `routerName`、`router` | |
| 10 | +> - 产品 **Products**:`POST/PUT /api/app/product` 支持可选 Body 字段 **`locationIds`**(多门店批量绑定 / 编辑时整表替换关联),详见 `项目相关文档/标签模块接口对接说明.md` **§6** | |
| 11 | +> | |
| 12 | +> 其余标签打印相关接口不在本文范围内。 | |
| 13 | + | |
| 14 | +--- | |
| 15 | + | |
| 16 | +## 1. 模板组件字段(`fl_label_template_element`) | |
| 17 | + | |
| 18 | +### 1.1 字段变更 | |
| 19 | + | |
| 20 | +- 新增字段:`TypeAdd`(元素附加类型,如 `label_Duration`) | |
| 21 | +- 新增字段:`ElementName`(元素名称,用于更稳定的显示/快照) | |
| 22 | + | |
| 23 | +### 1.2 接口影响范围 | |
| 24 | + | |
| 25 | +- 模板新增/编辑:保存 `elements[].typeAdd` / `elements[].elementName` | |
| 26 | +- 模板详情/预览:返回 `elements[].typeAdd` / `elements[].elementName` | |
| 27 | + | |
| 28 | +### 1.3 JSON 对齐(elements[]) | |
| 29 | + | |
| 30 | +| 前端字段 | 后端字段 | 说明 | | |
| 31 | +|---|---|---| | |
| 32 | +| `type` | `ElementType` | 元素类型 | | |
| 33 | +| `typeAdd` | `TypeAdd` | 元素附加类型 | | |
| 34 | +| `elementName` | `ElementName` | 元素名称 | | |
| 35 | + | |
| 36 | +--- | |
| 37 | + | |
| 38 | +## 2. 产品模块 Categories(Products → Categories) | |
| 39 | + | |
| 40 | +> 数据库侧你已完成:`fl_product_category` 新字段、`fl_product_category_location` 新表。 | |
| 41 | + | |
| 42 | +### 2.1 表结构要点 | |
| 43 | + | |
| 44 | +- `fl_product_category`(主表)关键字段: | |
| 45 | + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) | |
| 46 | + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组) | |
| 47 | + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串 | |
| 48 | + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson` | |
| 49 | + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) | |
| 50 | +- `fl_product_category_location`(关联表): | |
| 51 | + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店 | |
| 52 | + | |
| 53 | +### 2.2 CRUD 接口(字段扩展) | |
| 54 | + | |
| 55 | +接口路径不变,仅扩展字段。 | |
| 56 | + | |
| 57 | +#### 2.2.1 列表 | |
| 58 | + | |
| 59 | +- **方法**:`GET` | |
| 60 | +- **路径**:`/api/app/product-category` | |
| 61 | +- **列表行新增返回**: | |
| 62 | + - `displayText` | |
| 63 | + - `buttonAppearance` | |
| 64 | + - `categoryPhotoUrl` | |
| 65 | + - `availabilityType` | |
| 66 | + | |
| 67 | +#### 2.2.2 详情 | |
| 68 | + | |
| 69 | +- **方法**:`GET` | |
| 70 | +- **路径**:`/api/app/product-category/{id}` | |
| 71 | +- **新增返回字段**: | |
| 72 | + - `displayText` | |
| 73 | + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析) | |
| 74 | + - `availabilityType` | |
| 75 | + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) | |
| 76 | + | |
| 77 | +#### 2.2.3 新增 | |
| 78 | + | |
| 79 | +- **方法**:`POST` | |
| 80 | +- **路径**:`/api/app/product-category` | |
| 81 | +- **新增入参字段**: | |
| 82 | + - `displayText` | |
| 83 | + - `buttonAppearance`、`categoryPhotoUrl` | |
| 84 | + - `availabilityType` | |
| 85 | + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个) | |
| 86 | + | |
| 87 | +#### 2.2.4 编辑 | |
| 88 | + | |
| 89 | +- **方法**:`PUT` | |
| 90 | +- **路径**:`/api/app/product-category/{id}` | |
| 91 | +- **入参同新增** | |
| 92 | + | |
| 93 | +#### 2.2.5 删除 | |
| 94 | + | |
| 95 | +- **方法**:`DELETE` | |
| 96 | +- **路径**:`/api/app/product-category/{id}` | |
| 97 | +- **说明**:逻辑删除;若被产品引用会阻止删除(保持原行为) | |
| 98 | + | |
| 99 | +### 2.3 后端校验规则(本次新增) | |
| 100 | + | |
| 101 | +- `availabilityType` 仅允许 `ALL/SPECIFIED` | |
| 102 | + - `SPECIFIED` 时 `locationIds` 至少 1 个 | |
| 103 | +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定 | |
| 104 | + | |
| 105 | +--- | |
| 106 | + | |
| 107 | +## 3. 标签模块 Label Categories(Labels → Label Categories) | |
| 108 | + | |
| 109 | +> 数据库侧新增:`fl_label_category` 新字段、`fl_label_category_location` 新表。 | |
| 110 | + | |
| 111 | +### 3.1 表结构要点 | |
| 112 | + | |
| 113 | +- `fl_label_category`(主表)关键字段: | |
| 114 | + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) | |
| 115 | + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组) | |
| 116 | + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串 | |
| 117 | + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson` | |
| 118 | + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) | |
| 119 | +- `fl_label_category_location`(关联表): | |
| 120 | + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键) | |
| 121 | + | |
| 122 | +### 3.2 CRUD 接口(字段扩展) | |
| 123 | + | |
| 124 | +接口路径不变,仅扩展字段。 | |
| 125 | + | |
| 126 | +#### 3.2.1 列表 | |
| 127 | + | |
| 128 | +- **方法**:`GET` | |
| 129 | +- **路径**:`/api/app/label-category` | |
| 130 | +- **列表行新增返回**: | |
| 131 | + - `displayText` | |
| 132 | + - `buttonAppearance` | |
| 133 | + - `categoryPhotoUrl` | |
| 134 | + - `availabilityType` | |
| 135 | + | |
| 136 | +#### 3.2.2 详情 | |
| 137 | + | |
| 138 | +- **方法**:`GET` | |
| 139 | +- **路径**:`/api/app/label-category/{id}` | |
| 140 | +- **新增返回字段**: | |
| 141 | + - `displayText` | |
| 142 | + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析) | |
| 143 | + - `availabilityType` | |
| 144 | + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) | |
| 145 | + | |
| 146 | +#### 3.2.3 新增 | |
| 147 | + | |
| 148 | +- **方法**:`POST` | |
| 149 | +- **路径**:`/api/app/label-category` | |
| 150 | +- **新增入参字段**: | |
| 151 | + - `displayText` | |
| 152 | + - `buttonAppearance`、`categoryPhotoUrl` | |
| 153 | + - `availabilityType` | |
| 154 | + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个) | |
| 155 | + | |
| 156 | +#### 3.2.4 编辑 | |
| 157 | + | |
| 158 | +- **方法**:`PUT` | |
| 159 | +- **路径**:`/api/app/label-category/{id}` | |
| 160 | +- **入参同新增** | |
| 161 | + | |
| 162 | +#### 3.2.5 删除 | |
| 163 | + | |
| 164 | +- **方法**:`DELETE` | |
| 165 | +- **路径**:`/api/app/label-category/{id}` | |
| 166 | +- **说明**:逻辑删除;若被标签引用会阻止删除(保持原行为) | |
| 167 | + | |
| 168 | +### 3.3 后端校验规则(本次新增) | |
| 169 | + | |
| 170 | +- `availabilityType` 仅允许 `ALL/SPECIFIED` | |
| 171 | + - `SPECIFIED` 时 `locationIds` 至少 1 个 | |
| 172 | +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定 | |
| 173 | + | |
| 174 | +--- | |
| 175 | + | |
| 176 | +## 4. App 端 `GET /api/app/us-app-labeling/labeling-tree` | |
| 177 | + | |
| 178 | +- **L1(标签分类)节点**:返回 `categoryPhotoUrl`、`buttonAppearance`(均为库中字符串,**多为 JSON**,与 CRUD 一致);缺省或空时 `buttonAppearance` 后端默认 **`"TEXT"`**(兼容旧数据,**不再**对整段做 `ToUpperInvariant` 以免破坏 JSON)。 | |
| 179 | +- **L2(产品分类)节点**:返回 `displayText`、`buttonAppearance`、`categoryPhotoUrl`、`availabilityType`、`orderNum` 等;外观数据由 **`buttonAppearance` + `categoryPhotoUrl`** 承载(已不再返回 `buttonTextColor`、`buttonBgColor`、`buttonImageUrl`、`buttonStyleJson`)。 | |
| 180 | + | |
| 181 | +### 4.1 数据库迁移(两张主表) | |
| 182 | + | |
| 183 | +在确认历史数据已按需迁到 `CategoryPhotoUrl` 后,可执行(列不存在时需跳过或调整): | |
| 184 | + | |
| 185 | +```sql | |
| 186 | +ALTER TABLE `fl_label_category` | |
| 187 | + DROP COLUMN `ButtonTextColor`, | |
| 188 | + DROP COLUMN `ButtonBgColor`, | |
| 189 | + DROP COLUMN `ButtonImageUrl`, | |
| 190 | + DROP COLUMN `ButtonStyleJson`; | |
| 191 | + | |
| 192 | +ALTER TABLE `fl_product_category` | |
| 193 | + DROP COLUMN `ButtonTextColor`, | |
| 194 | + DROP COLUMN `ButtonBgColor`, | |
| 195 | + DROP COLUMN `ButtonImageUrl`, | |
| 196 | + DROP COLUMN `ButtonStyleJson`; | |
| 197 | +``` | |
| 198 | + | |
| 199 | +--- | |
| 200 | + | |
| 201 | +## 5. Web 管理端会话(`AuthSession` / 食品标签-美国版) | |
| 202 | + | |
| 203 | +> 实现:`IAuthSessionAppService` / `AuthSessionAppService`。需携带与后台一致的 **JWT**(`Authorization: Bearer {token}`)。具体 action 路径以部署环境 **Swagger / OpenAPI** 为准;下列为 ABP 常规约定(`RootPath = api/app`)。 | |
| 204 | + | |
| 205 | +### 5.1 获取当前登录用户菜单与权限 | |
| 206 | + | |
| 207 | +- **方法**:`GET` | |
| 208 | +- **路径**(约定):`/api/app/auth-session/my-menus` | |
| 209 | +- **鉴权**:需要登录 | |
| 210 | +- **用途**:前端动态路由、侧边栏、按钮级权限(`permissionCodes`) | |
| 211 | +- **返回体**(`CurrentUserMenuPermissionsOutputDto`,JSON 字段名为 camelCase): | |
| 212 | + | |
| 213 | +| 字段 | 类型 | 说明 | | |
| 214 | +|---|---|---| | |
| 215 | +| `user` | object | 当前用户简要信息(无密码) | | |
| 216 | +| `user.id` | guid | 用户 Id | | |
| 217 | +| `user.userName` | string | 登录名 | | |
| 218 | +| `user.nick` | string? | 昵称 | | |
| 219 | +| `user.email` | string? | 邮箱 | | |
| 220 | +| `user.icon` | string? | 头像 | | |
| 221 | +| `roleCodes` | string[] | 角色编码列表(已排序) | | |
| 222 | +| `permissionCodes` | string[] | 权限码列表(已排序;超级管理员常见为 `*:*:*`) | | |
| 223 | +| `menus` | array | **菜单树**(根节点列表,子节点在 `children`) | | |
| 224 | + | |
| 225 | +**菜单树节点**(`CurrentUserMenuNodeDto`)主要字段: | |
| 226 | + | |
| 227 | +| 字段 | 说明 | | |
| 228 | +|---|---| | |
| 229 | +| `id` / `parentId` | 菜单 Id、父 Id(根父级多为 `"0"`) | | |
| 230 | +| `menuName` | 菜单名称 | | |
| 231 | +| `routerName` / `router` | 路由名、路径 | | |
| 232 | +| `permissionCode` | 权限标识(按钮/接口控制用) | | |
| 233 | +| `menuType` / `menuSource` | 枚举整型值(与 `Menu` 表一致) | | |
| 234 | +| `orderNum` / `state` | 排序、是否启用 | | |
| 235 | +| `menuIcon` / `component` / `isLink` / `isCache` / `isShow` / `query` / `remark` | 与菜单表一致 | | |
| 236 | +| `children` | 子节点数组 | | |
| 237 | + | |
| 238 | +**业务说明**: | |
| 239 | + | |
| 240 | +- 数据来源与框架 `UserManager.GetInfoAsync` 一致:按用户角色合并菜单与权限码。 | |
| 241 | +- 用户名为 **`admin`** 时:与 `AccountService.GetVue3Router` 对齐,返回 **`Menu` 表中未逻辑删除** 的全部菜单再组树(`permissionCodes` 仍为超级管理员约定值)。 | |
| 242 | + | |
| 243 | +### 5.2 退出登录 | |
| 244 | + | |
| 245 | +- **方法**:`POST` | |
| 246 | +- **路径**(约定):`/api/app/auth-session/logout` | |
| 247 | +- **鉴权**:需要登录(未登录或无法解析用户时返回 `false`) | |
| 248 | +- **请求体**:无 | |
| 249 | +- **返回**:`boolean` | |
| 250 | + - `true`:已清除服务端 **用户信息分布式缓存**(与 `AccountService.PostLogout` 一致) | |
| 251 | + - `false`:当前请求未识别到用户 Id(例如未登录) | |
| 252 | +- **说明**:JWT 为无状态令牌,**前端仍需丢弃本地 Token**;退出接口主要清理服务端缓存侧用户信息。 | |
| 253 | + | |
| 254 | +--- | |
| 255 | + | |
| 256 | +## 6. 权限菜单 `rbac-menu` 列表/详情补充字段 | |
| 257 | + | |
| 258 | +- **路径**:`GET /api/app/rbac-menu`(列表)、`GET /api/app/rbac-menu/{id}`(详情) | |
| 259 | +- **新增返回**(与 `menu` 表字段一致,JSON 一般为 camelCase): | |
| 260 | + - `routerName`:路由名称 | |
| 261 | + - `router`:路由路径 | |
| 262 | +- **说明**:树接口 `GET /api/app/rbac-menu/tree`(若已使用)本身已包含完整菜单字段,无需重复改动。 | |
| 263 | + | ... | ... |
项目相关文档/标签模块接口对接说明.md
| ... | ... | @@ -51,6 +51,12 @@ Swagger 地址: |
| 51 | 51 | } |
| 52 | 52 | ``` |
| 53 | 53 | |
| 54 | +### 1.1.1 字段约定:`buttonAppearance` 与 `categoryPhotoUrl`(JSON 字符串) | |
| 55 | + | |
| 56 | +- **`buttonAppearance`**:库中存 **JSON 文本**(如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]` 等);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 `["TEXT"]` 等)。未传或空白时后端默认 `["TEXT"]`。非法值(非 JSON 且非上述三者)会返回友好错误。 | |
| 57 | +- **`categoryPhotoUrl`**:同样为 **JSON 文本**(如 `["Prep","#10B981"]`);若传**非 JSON** 的纯文本(色值、`/picture/...` 等),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**字符串,由客户端解析。 | |
| 58 | +- 其它常用字段:`displayText`、`availabilityType`(`ALL`/`SPECIFIED`)、`locationIds`(指定门店时必填),与产品类别接口语义一致(见 `项目相关文档/产品模块Categories接口对接说明.md`)。 | |
| 59 | + | |
| 54 | 60 | ### 1.2 详情 |
| 55 | 61 | |
| 56 | 62 | 方法:`GET /api/app/label-category/{id}` |
| ... | ... | @@ -69,7 +75,11 @@ Swagger 地址: |
| 69 | 75 | { |
| 70 | 76 | "categoryCode": "CAT_PREP", |
| 71 | 77 | "categoryName": "Prep", |
| 72 | - "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png", | |
| 78 | + "displayText": "Prep", | |
| 79 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 80 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 81 | + "availabilityType": "ALL", | |
| 82 | + "locationIds": [], | |
| 73 | 83 | "state": true, |
| 74 | 84 | "orderNum": 1 |
| 75 | 85 | } |
| ... | ... | @@ -85,7 +95,11 @@ Swagger 地址: |
| 85 | 95 | { |
| 86 | 96 | "categoryCode": "CAT_PREP", |
| 87 | 97 | "categoryName": "Prep", |
| 88 | - "categoryPhotoUrl": null, | |
| 98 | + "displayText": "Prep", | |
| 99 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 100 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 101 | + "availabilityType": "ALL", | |
| 102 | + "locationIds": [], | |
| 89 | 103 | "state": true, |
| 90 | 104 | "orderNum": 2 |
| 91 | 105 | } |
| ... | ... | @@ -560,6 +574,10 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |
| 560 | 574 | 入参: |
| 561 | 575 | - `id`:产品Id(`fl_product.Id`) |
| 562 | 576 | |
| 577 | +返回(`ProductGetOutputDto`,与实现一致的主要字段): | |
| 578 | +- `id`、`productCode`、`productName`、`categoryId`、`categoryName`、`productImageUrl`、`state` | |
| 579 | +- **`locationIds`**:`string[]`,该产品在 **`fl_location_product`** 中绑定的门店 Id(去重);无关联时为空数组 | |
| 580 | + | |
| 563 | 581 | ### 6.3 新增产品 |
| 564 | 582 | |
| 565 | 583 | 方法:`POST /api/app/product` |
| ... | ... | @@ -569,15 +587,30 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |
| 569 | 587 | { |
| 570 | 588 | "productCode": "PRD_TEST_001", |
| 571 | 589 | "productName": "Chicken", |
| 572 | - "categoryName": "Meat", | |
| 590 | + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | |
| 573 | 591 | "productImageUrl": "https://example.com/img.png", |
| 574 | - "state": true | |
| 592 | + "state": true, | |
| 593 | + "locationIds": [ | |
| 594 | + "11111111-1111-1111-1111-111111111111", | |
| 595 | + "22222222-2222-2222-2222-222222222222" | |
| 596 | + ] | |
| 575 | 597 | } |
| 576 | 598 | ``` |
| 577 | 599 | |
| 600 | +字段说明: | |
| 601 | +| 字段 | 类型 | 必填 | 说明 | | |
| 602 | +|------|------|------|------| | |
| 603 | +| `productCode` | string | 是 | 产品编码 | | |
| 604 | +| `productName` | string | 是 | 产品名称 | | |
| 605 | +| `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | | |
| 606 | +| `productImageUrl` | string \| null | 否 | 主图 URL | | |
| 607 | +| `state` | bool | 否 | 默认 `true` | | |
| 608 | +| **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | | |
| 609 | + | |
| 578 | 610 | 校验: |
| 579 | -- `productCode/productName` 不能为空 | |
| 611 | +- `productCode` / `productName` 不能为空 | |
| 580 | 612 | - `productCode` 不能与未删除的数据重复 |
| 613 | +- 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) | |
| 581 | 614 | |
| 582 | 615 | ### 6.4 编辑产品 |
| 583 | 616 | |
| ... | ... | @@ -585,7 +618,13 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |
| 585 | 618 | |
| 586 | 619 | 入参: |
| 587 | 620 | - Path:`id` 为当前产品Id(`fl_product.Id`) |
| 588 | -- Body:字段同新增(`ProductUpdateInputVo`) | |
| 621 | +- Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) | |
| 622 | + | |
| 623 | +**`locationIds` 行为(与新增不同,请注意):** | |
| 624 | +- **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 | |
| 625 | +- **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 | |
| 626 | + | |
| 627 | +其它校验同 **§6.3**(含门店存在性校验,当 `locationIds` 含非空项时)。 | |
| 589 | 628 | |
| 590 | 629 | ### 6.5 删除(逻辑删除) |
| 591 | 630 | |
| ... | ... | @@ -599,6 +638,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |
| 599 | 638 | |
| 600 | 639 | 说明: |
| 601 | 640 | - 关联表:`fl_location_product` |
| 641 | +- 也可在 **§6.3 / §6.4** 通过产品 Body 的 **`locationIds`** 一次性维护本产品在各门店的关联(与 §7 写入同一张表);二者可并存,按需选择调用方式。 | |
| 602 | 642 | - 关联按门店进行批量替换: |
| 603 | 643 | - `Create`:在门店下新增未存在的 product 关联 |
| 604 | 644 | - `Update`:替换该门店下全部关联(先删后建) |
| ... | ... | @@ -706,7 +746,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |
| 706 | 746 | |------|------|------| |
| 707 | 747 | | `id` | string | `fl_label_category.Id` | |
| 708 | 748 | | `categoryName` | string | 分类名称 | |
| 709 | -| `categoryPhotoUrl` | string \| null | 分类图标/图 | | |
| 749 | +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) | | |
| 750 | +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) | | |
| 710 | 751 | | `orderNum` | number | 排序 | |
| 711 | 752 | | `productCategories` | array | 第二级列表(见下表) | |
| 712 | 753 | |
| ... | ... | @@ -715,8 +756,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |
| 715 | 756 | | 字段 | 类型 | 说明 | |
| 716 | 757 | |------|------|------| |
| 717 | 758 | | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | |
| 718 | -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | | |
| 759 | +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 | | |
| 719 | 760 | | `name` | string | 产品分类显示名;空源数据为 **`无`** | |
| 761 | +| `displayText` | string \| null | 按钮展示文案 | | |
| 762 | +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | | |
| 763 | +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | | |
| 764 | +| `orderNum` | number | 排序 | | |
| 720 | 765 | | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | |
| 721 | 766 | | `products` | array | 第三级产品列表(见下表) | |
| 722 | 767 | |
| ... | ... | @@ -771,13 +816,18 @@ curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati |
| 771 | 816 | { |
| 772 | 817 | "id": "cat-prep-id", |
| 773 | 818 | "categoryName": "Prep", |
| 774 | - "categoryPhotoUrl": "/picture/...", | |
| 819 | + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", | |
| 820 | + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", | |
| 775 | 821 | "orderNum": 1, |
| 776 | 822 | "productCategories": [ |
| 777 | 823 | { |
| 778 | 824 | "categoryId": "pc-meat-id", |
| 779 | - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", | |
| 825 | + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]", | |
| 780 | 826 | "name": "Meat", |
| 827 | + "displayText": "Meat", | |
| 828 | + "buttonAppearance": "[\"IMAGE\"]", | |
| 829 | + "availabilityType": "ALL", | |
| 830 | + "orderNum": 10, | |
| 781 | 831 | "itemCount": 1, |
| 782 | 832 | "products": [ |
| 783 | 833 | { | ... | ... |
项目相关文档/美国版App登录接口说明.md
| ... | ... | @@ -149,11 +149,294 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
| 149 | 149 | |
| 150 | 150 | --- |
| 151 | 151 | |
| 152 | +## 接口 3:我的资料(My Profile) | |
| 153 | + | |
| 154 | +用于「My Profile」页展示:全名、邮箱、电话、员工号、角色。 | |
| 155 | + | |
| 156 | +### HTTP | |
| 157 | + | |
| 158 | +- **方法**:`GET` | |
| 159 | +- **路径**:`/api/app/us-app-auth/my-profile`(以 Swagger 中 `UsAppAuth` 为准) | |
| 160 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`) | |
| 161 | + | |
| 162 | +### 请求参数 | |
| 163 | + | |
| 164 | +无。 | |
| 165 | + | |
| 166 | +### 响应体(UsAppMyProfileOutputDto) | |
| 167 | + | |
| 168 | +| 字段(JSON) | 类型 | 说明 | | |
| 169 | +|--------------|------|------| | |
| 170 | +| `fullName` | string | `User.Name`,无则 `Nick`,再无则 `UserName` | | |
| 171 | +| `email` | string | `User.Email`;空为 `"无"` | | |
| 172 | +| `phone` | string | 由 `User.Phone`(long)格式化为可读字符串;10 位数字按 `+1 (555) 123-4567` 形式;无则 `"无"` | | |
| 173 | +| `employeeId` | string | 当前取 **`User.UserName`**(可与业务约定为工号/登录名) | | |
| 174 | +| `roleDisplay` | string | 多角色 `Role.RoleName` 按 `OrderNum` 排序后以英文逗号拼接;无角色为 `"无"` | | |
| 175 | +| `primaryRoleCode` | string \| null | 第一个角色的 `RoleCode`,供前端样式 | | |
| 176 | + | |
| 177 | +--- | |
| 178 | + | |
| 179 | +## 接口 4:修改密码(Change Password) | |
| 180 | + | |
| 181 | +与 **`User.JudgePassword` / `BuildPassword`** 一致:校验当前密码后写入新盐值哈希。 | |
| 182 | + | |
| 183 | +### HTTP | |
| 184 | + | |
| 185 | +- **方法**:`POST` | |
| 186 | +- **路径**:`/api/app/us-app-auth/change-password` | |
| 187 | +- **Content-Type**:`application/json` | |
| 188 | +- **鉴权**:需要登录 | |
| 189 | + | |
| 190 | +### 请求体(UsAppChangePasswordInputVo) | |
| 191 | + | |
| 192 | +| 字段(JSON) | 类型 | 必填 | 说明 | | |
| 193 | +|--------------|------|------|------| | |
| 194 | +| `currentPassword` | string | 是 | 当前明文密码 | | |
| 195 | +| `newPassword` | string | 是 | 新密码 | | |
| 196 | +| `confirmNewPassword` | string | 是 | 必须与 `newPassword` **完全一致** | | |
| 197 | + | |
| 198 | +### 新密码复杂度(与原型「Password Requirements」一致) | |
| 199 | + | |
| 200 | +1. 至少 **8** 位 | |
| 201 | +2. 至少 **1** 个大写字母、**1** 个小写字母 | |
| 202 | +3. 至少 **1** 个数字 | |
| 203 | +4. 至少 **1** 个**非字母数字**字符(特殊字符) | |
| 204 | + | |
| 205 | +### 请求示例 | |
| 206 | + | |
| 207 | +```json | |
| 208 | +{ | |
| 209 | + "currentPassword": "OldPass1!", | |
| 210 | + "newPassword": "NewPass9@x", | |
| 211 | + "confirmNewPassword": "NewPass9@x" | |
| 212 | +} | |
| 213 | +``` | |
| 214 | + | |
| 215 | +### 响应 | |
| 216 | + | |
| 217 | +成功时 HTTP 200,无业务体要求(ABP 可能返回空对象);失败为业务异常文案。 | |
| 218 | + | |
| 219 | +### 常见错误 | |
| 220 | + | |
| 221 | +- 未登录:`用户未登录` | |
| 222 | +- 当前密码错误:与登录失败一致(`UserConst.Login_Error` 文案) | |
| 223 | +- 新密码与确认不一致:`新密码与确认密码不一致` | |
| 224 | +- 新密码与当前相同:`新密码不能与当前密码相同` | |
| 225 | +- 不满足复杂度:如 `新密码至少 8 位`、`新密码需包含大写字母` 等 | |
| 226 | + | |
| 227 | +--- | |
| 228 | + | |
| 229 | +## 接口 5:门店详情(Location) | |
| 230 | + | |
| 231 | +用于移动端「Location」页:店名、完整地址、门店电话、营业时间占位、店长姓名与电话。**仅当当前 JWT 用户在 `userlocation` 中绑定该门店**时可查(与接口 1、2 的门店范围一致)。 | |
| 232 | + | |
| 233 | +### HTTP | |
| 234 | + | |
| 235 | +- **方法**:`GET` | |
| 236 | +- **路径**:`/api/app/us-app-auth/location-detail/{locationId}`(以 Swagger 中 `UsAppAuth` → `GetLocationDetail` 为准) | |
| 237 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`) | |
| 238 | + | |
| 239 | +### 请求参数 | |
| 240 | + | |
| 241 | +| 参数名 | 位置 | 类型 | 必填 | 说明 | | |
| 242 | +|--------|------|------|------|------| | |
| 243 | +| `locationId` | Path | string | 是 | 门店主键,Guid 字符串(与 `locations[].id` 一致) | | |
| 244 | + | |
| 245 | +### 传参示例 | |
| 246 | + | |
| 247 | +```http | |
| 248 | +GET /api/app/us-app-auth/location-detail/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1 | |
| 249 | +Host: localhost:19001 | |
| 250 | +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | |
| 251 | +``` | |
| 252 | + | |
| 253 | +### 响应体(UsAppLocationDetailOutputDto) | |
| 254 | + | |
| 255 | +| 字段(JSON) | 类型 | 说明 | | |
| 256 | +|--------------|------|------| | |
| 257 | +| `locationId` | string | 门店主键 | | |
| 258 | +| `locationName` | string | 门店名称;空为 `"无"` | | |
| 259 | +| `fullAddress` | string | 与登录接口相同的地址拼接规则;无有效片段为 `"无"` | | |
| 260 | +| `storePhone` | string | `location.Phone` 去首尾空格;空为 `"无"` | | |
| 261 | +| `operatingHours` | string | 当前 **`location` 表无营业时间字段**,固定返回 **`"无"`**(后续若落库可再对接) | | |
| 262 | +| `managerName` | string | 在同店 `userlocation` 绑定用户中,取角色 `RoleCode` / `RoleName`(忽略大小写)包含 **`manager`** 的第一人展示姓名(`Name` → `Nick` → `UserName`);无匹配为 **`"无"`** | | |
| 263 | +| `managerPhone` | string | 同上用户的 `User.Phone`(long)按与「我的资料」相同的可读格式;无电话为 **`"无"`** | | |
| 264 | + | |
| 265 | +### 响应示例 | |
| 266 | + | |
| 267 | +```json | |
| 268 | +{ | |
| 269 | + "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | |
| 270 | + "locationName": "Downtown Store", | |
| 271 | + "fullAddress": "123 Main St, New York, NY 10001", | |
| 272 | + "storePhone": "(212) 555-0100", | |
| 273 | + "operatingHours": "无", | |
| 274 | + "managerName": "Jane Doe", | |
| 275 | + "managerPhone": "+1 (555) 123-4567" | |
| 276 | +} | |
| 277 | +``` | |
| 278 | + | |
| 279 | +### 常见错误(业务异常文案) | |
| 280 | + | |
| 281 | +- 未登录:`用户未登录` | |
| 282 | +- `locationId` 非法或空:`无效的门店标识` | |
| 283 | +- 当前用户未绑定该门店:`当前账号未绑定该门店,无法查看` | |
| 284 | +- 门店已删或不存在:`门店不存在或已删除` | |
| 285 | + | |
| 286 | +--- | |
| 287 | + | |
| 288 | +## 接口 6:全局 Support 联系方式(App 只读 + Web 可读) | |
| 289 | + | |
| 290 | +用于 App「Support」页与 Web 展示**全平台共用**的一条电话与邮箱。 | |
| 291 | +实现:`LocationSupportAppService`,表 `fl_location_support` **不再包含门店 Id**。 | |
| 292 | + | |
| 293 | +### HTTP(查询) | |
| 294 | + | |
| 295 | +- **方法**:`GET` | |
| 296 | +- **路径**(约定式 API,以 Swagger 中 `LocationSupport` → `GetSupport` 为准):一般为 **`/api/app/location-support/support`** | |
| 297 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`)。**App 登录 Token 与 Web Token 均可调用本接口。** | |
| 298 | + | |
| 299 | +### 请求参数 | |
| 300 | + | |
| 301 | +无 Query / Body。 | |
| 302 | + | |
| 303 | +### 响应体(LocationSupportGetOutputDto) | |
| 304 | + | |
| 305 | +| 字段(JSON) | 类型 | 说明 | | |
| 306 | +|--------------|------|------| | |
| 307 | +| `id` | string | 记录主键(Web 编辑 `PUT` 路径中的 `{id}`) | | |
| 308 | +| `supportPhone` | string | Support 电话 | | |
| 309 | +| `supportEmail` | string | Support 邮箱 | | |
| 310 | + | |
| 311 | +> 若尚未在后台配置,接口返回 `null`。 | |
| 312 | + | |
| 313 | +### 响应示例 | |
| 314 | + | |
| 315 | +```json | |
| 316 | +{ | |
| 317 | + "id": "3a2f4fda-1a93-4a35-9b98-95dca7bb5d2a", | |
| 318 | + "supportPhone": "1-800-SUPPORT", | |
| 319 | + "supportEmail": "support@medvantage.com" | |
| 320 | +} | |
| 321 | +``` | |
| 322 | + | |
| 323 | +### App 与 Web 权限说明 | |
| 324 | + | |
| 325 | +- App 登录签发 JWT 时会写入声明 **`client_kind` = `us-app`**(与 Web 管理端 Token 区分)。 | |
| 326 | +- **App 仅允许调用本节的 `GET`(查询)**;若使用 App Token 调用新增/编辑,将返回业务错误(英文):`The mobile app can only view support contacts. Please use the web console to edit.` | |
| 327 | + | |
| 328 | +--- | |
| 329 | + | |
| 330 | +## 后台维护接口:Location Support(Web:新增 / 编辑) | |
| 331 | + | |
| 332 | +仅 **Web 管理端 Token**(无 `client_kind=us-app`)可调用,用于维护全局 Support 联系方式。 | |
| 333 | + | |
| 334 | +### 接口 A:新增(全局一条) | |
| 335 | + | |
| 336 | +- **方法**:`POST` | |
| 337 | +- **路径**:`/api/app/location-support` | |
| 338 | +- **Content-Type**:`application/json` | |
| 339 | + | |
| 340 | +请求体(LocationSupportCreateInputVo): | |
| 341 | + | |
| 342 | +```json | |
| 343 | +{ | |
| 344 | + "supportPhone": "1-800-SUPPORT", | |
| 345 | + "supportEmail": "support@medvantage.com" | |
| 346 | +} | |
| 347 | +``` | |
| 348 | + | |
| 349 | +约束: | |
| 350 | + | |
| 351 | +- 系统内仅允许存在一条未删除记录;若已存在,再次新增会报错:`Global support contact already exists. Use update instead.` | |
| 352 | + | |
| 353 | +### 接口 B:编辑 | |
| 354 | + | |
| 355 | +- **方法**:`PUT` | |
| 356 | +- **路径**:`/api/app/location-support/{id}` | |
| 357 | +- **Content-Type**:`application/json` | |
| 358 | + | |
| 359 | +请求体(LocationSupportUpdateInputVo): | |
| 360 | + | |
| 361 | +```json | |
| 362 | +{ | |
| 363 | + "supportPhone": "1-800-SUPPORT", | |
| 364 | + "supportEmail": "support@medvantage.com" | |
| 365 | +} | |
| 366 | +``` | |
| 367 | + | |
| 368 | +常见错误(英文): | |
| 369 | + | |
| 370 | +- `The mobile app can only view support contacts. Please use the web console to edit.` | |
| 371 | +- `Support phone is required.` / `Support email is required.` / `Support email format is invalid.` | |
| 372 | +- `Global support contact already exists. Use update instead.` | |
| 373 | +- `Support record not found.` / `Support record id is required.` | |
| 374 | + | |
| 375 | +### 数据库迁移(删除 `LocationId`) | |
| 376 | + | |
| 377 | +若线上表仍为旧结构(含 `LocationId`),请在库中执行脚本: | |
| 378 | + | |
| 379 | +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql` | |
| 380 | + | |
| 381 | +新建库请使用: | |
| 382 | + | |
| 383 | +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql` | |
| 384 | + | |
| 385 | +--- | |
| 386 | + | |
| 387 | +## 平台端登录接口(Web) | |
| 388 | + | |
| 389 | +> 对应 `RBAC.AccountService.PostLoginAsync`。当前平台端登录规则已调整为:**仅支持 Email 作为账号,不再支持 UserName**。 | |
| 390 | + | |
| 391 | +### HTTP | |
| 392 | + | |
| 393 | +- **方法**:`POST` | |
| 394 | +- **路径**:`/api/app/account/login`(以 Swagger 中 `Account` 为准) | |
| 395 | +- **Content-Type**:`application/json` | |
| 396 | +- **鉴权**:匿名 | |
| 397 | + | |
| 398 | +### 请求体(LoginInputVo) | |
| 399 | + | |
| 400 | +| 字段(JSON) | 类型 | 必填 | 说明 | | |
| 401 | +|--------------|------|------|------| | |
| 402 | +| `userName` | string | 是 | 兼容历史字段名;现必须传 **邮箱**(例如 `admin@example.com`) | | |
| 403 | +| `password` | string | 是 | 密码 | | |
| 404 | +| `uuid` | string | 条件 | 开启图形验证码时必填 | | |
| 405 | +| `code` | string | 条件 | 开启图形验证码时必填 | | |
| 406 | + | |
| 407 | +### 请求示例 | |
| 408 | + | |
| 409 | +```json | |
| 410 | +{ | |
| 411 | + "userName": "admin@example.com", | |
| 412 | + "password": "YourPassword", | |
| 413 | + "uuid": "captcha-uuid", | |
| 414 | + "code": "captcha-code" | |
| 415 | +} | |
| 416 | +``` | |
| 417 | + | |
| 418 | +### 响应体(LoginOutputDto) | |
| 419 | + | |
| 420 | +| 字段(JSON) | 类型 | 说明 | | |
| 421 | +|--------------|------|------| | |
| 422 | +| `token` | string | Access Token | | |
| 423 | +| `refreshToken` | string | Refresh Token | | |
| 424 | + | |
| 425 | +### 错误文案(英文) | |
| 426 | + | |
| 427 | +- `Email and password are required.` | |
| 428 | +- `Sign-in failed: email is required as the account.` | |
| 429 | +- `Sign-in failed: account not found.` | |
| 430 | +- `Sign-in failed: incorrect email or password.` | |
| 431 | +- `Invalid captcha.` | |
| 432 | + | |
| 433 | +--- | |
| 434 | + | |
| 152 | 435 | ## 与其他登录方式的区别 |
| 153 | 436 | |
| 154 | 437 | | 场景 | 说明 | |
| 155 | 438 | |------|------| |
| 156 | -| Web 管理端 | 仍使用 RBAC **`AccountService.PostLoginAsync`**,一般为人 **`userName`** + 密码 | | |
| 157 | -| 美国版 App | **仅**本模块 **`/api/app/us-app-auth/login`** 使用 **邮箱 + 密码** | | |
| 439 | +| Web 管理端 | 使用 RBAC **`/api/app/account/login`**,字段名虽为 `userName`,但值必须是 **Email** | | |
| 440 | +| 美国版 App | 使用 **`/api/app/us-app-auth/login`**,显式字段 `email` + `password` | | |
| 158 | 441 | |
| 159 | 442 | 两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。 | ... | ... |