diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs new file mode 100644 index 0000000..4567f6d --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs @@ -0,0 +1,17 @@ +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; + +/// +/// 当前登录用户简要信息(不含敏感字段) +/// +public class CurrentUserBriefDto +{ + public Guid Id { get; set; } + + public string UserName { get; set; } = string.Empty; + + public string? Nick { get; set; } + + public string? Email { get; set; } + + public string? Icon { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuNodeDto.cs new file mode 100644 index 0000000..b7935f7 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuNodeDto.cs @@ -0,0 +1,43 @@ +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; + +/// +/// 当前用户可见菜单树节点(与权限分配一致) +/// +public class CurrentUserMenuNodeDto +{ + public string Id { get; set; } = string.Empty; + + public string ParentId { get; set; } = "0"; + + public string MenuName { get; set; } = string.Empty; + + public string? RouterName { get; set; } + + public string? Router { get; set; } + + public string? PermissionCode { get; set; } + + public int MenuType { get; set; } + + public int MenuSource { get; set; } + + public int OrderNum { get; set; } + + public bool State { get; set; } + + public string? MenuIcon { get; set; } + + public string? Component { get; set; } + + public bool IsLink { get; set; } + + public bool IsCache { get; set; } + + public bool IsShow { get; set; } + + public string? Query { get; set; } + + public string? Remark { get; set; } + + public List Children { get; set; } = new(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs new file mode 100644 index 0000000..5ea17eb --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs @@ -0,0 +1,15 @@ +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; + +/// +/// 当前登录用户的菜单与权限码(用于前端动态路由/按钮权限) +/// +public class CurrentUserMenuPermissionsOutputDto +{ + public CurrentUserBriefDto User { get; set; } = new(); + + public List RoleCodes { get; set; } = new(); + + public List PermissionCodes { get; set; } = new(); + + public List Menus { get; set; } = new(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardCategoryDistributionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardCategoryDistributionDto.cs new file mode 100644 index 0000000..755ec97 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardCategoryDistributionDto.cs @@ -0,0 +1,19 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; + +/// +/// 分类分布项 +/// +public class DashboardCategoryDistributionDto +{ + /// 分类Id + public string CategoryId { get; set; } = string.Empty; + + /// 分类名称 + public string CategoryName { get; set; } = string.Empty; + + /// 数量 + public int Count { get; set; } + + /// 占比(百分比) + public decimal Ratio { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardDailyTrendPointDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardDailyTrendPointDto.cs new file mode 100644 index 0000000..1c58d7c --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardDailyTrendPointDto.cs @@ -0,0 +1,13 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; + +/// +/// 日趋势点 +/// +public class DashboardDailyTrendPointDto +{ + /// 日期(yyyy-MM-dd) + public string Date { get; set; } = string.Empty; + + /// + public int Value { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardMetricCardDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardMetricCardDto.cs new file mode 100644 index 0000000..c6807c0 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardMetricCardDto.cs @@ -0,0 +1,25 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; + +/// +/// 仪表盘指标卡片 +/// +public class DashboardMetricCardDto +{ + /// 指标唯一标识(如 labelsPrintedToday) + public string Key { get; set; } = string.Empty; + + /// 指标标题 + public string Title { get; set; } = string.Empty; + + /// 当前值 + public int Value { get; set; } + + /// 对比周期值 + public int PreviousValue { get; set; } + + /// 增减值(Value - PreviousValue) + public int ChangeValue { get; set; } + + /// 增减比例(百分比) + public decimal ChangeRate { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardOverviewOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardOverviewOutputDto.cs new file mode 100644 index 0000000..195477e --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardOverviewOutputDto.cs @@ -0,0 +1,49 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; + +/// +/// 仪表盘总览输出 +/// +public class DashboardOverviewOutputDto +{ + /// 今日打印标签 + public DashboardMetricCardDto LabelsPrintedToday { get; set; } = new(); + + /// 启用模板数 + public DashboardMetricCardDto ActiveTemplates { get; set; } = new(); + + /// 活跃用户数 + public DashboardMetricCardDto ActiveUsers { get; set; } = new(); + + /// 门店数 + public DashboardMetricCardDto Locations { get; set; } = new(); + + /// 人员数 + public DashboardMetricCardDto People { get; set; } = new(); + + /// 产品数 + public DashboardMetricCardDto Products { get; set; } = new(); + + /// 指标卡片 + public List MetricCards { get; set; } = new(); + + /// 近7天打印趋势 + public List WeeklyPrintVolume { get; set; } = new(); + + /// 按分类分布 + public List CategoryDistribution { get; set; } = new(); + + /// 分类分布总数 + public int CategoryDistributionTotal { get; set; } + + /// 按分类分布(前端直观命名) + public List ByCategory { get; set; } = new(); + + /// 按分类分布总数(前端直观命名) + public int ByCategoryTotal { get; set; } + + /// 最近打印标签(全门店最新若干条,用于 Recent Labels 区块) + public List RecentLabels { get; set; } = new(); + + /// 统计时间 + public DateTime GeneratedAt { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs new file mode 100644 index 0000000..a3a6de6 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs @@ -0,0 +1,31 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; + +/// +/// Dashboard「Recent Labels」单行数据(最近打印记录) +/// +public class DashboardRecentLabelItemDto +{ + /// 打印任务 Id(fl_label_print_task.Id) + public string TaskId { get; set; } = string.Empty; + + /// 标签编码(界面 Serial / Label ID,如 1-251201) + public string LabelCode { get; set; } = string.Empty; + + /// 展示名称:优先产品名,否则标签名称 + public string DisplayName { get; set; } = "无"; + + /// 打印人用户 Id(CreatedBy) + public string? PrintedByUserId { get; set; } + + /// 打印人展示名(User.Name 或 UserName) + public string PrintedByName { get; set; } = "无"; + + /// 打印时间(PrintedAt 优先,否则 CreationTime) + public DateTime PrintedAt { get; set; } + + /// 状态:activeexpired(依据 PrintInputJson 中保质期与当前日期比较) + public string Status { get; set; } = "active"; + + /// 角标/尺寸短文案(如 2"x2",用于左侧圆标) + public string LabelTypeBadge { get; set; } = "无"; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs new file mode 100644 index 0000000..5b75864 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs @@ -0,0 +1,16 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Group; + +/// +/// 新建组织入参 +/// +public class GroupCreateInputVo +{ + public string GroupName { get; set; } = string.Empty; + + /// + /// 指派到的合作伙伴 Id(Assign to Partner) + /// + public string PartnerId { get; set; } = string.Empty; + + public bool State { get; set; } = true; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs new file mode 100644 index 0000000..6bfc39b --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs @@ -0,0 +1,24 @@ +using Volo.Abp.Application.Dtos; + +namespace FoodLabeling.Application.Contracts.Dtos.Group; + +/// +/// 组织(Group)分页查询入参 +/// +public class GroupGetListInputVo : PagedAndSortedResultRequestDto +{ + /// + /// 模糊搜索(GroupName、所属 Partner 的 PartnerName) + /// + public string? Keyword { get; set; } + + /// + /// 按所属合作伙伴筛选(fl_partner.Id) + /// + public string? PartnerId { get; set; } + + /// + /// 启用状态 + /// + public bool? State { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs new file mode 100644 index 0000000..0971594 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs @@ -0,0 +1,22 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Group; + +/// +/// 组织列表项 +/// +public class GroupGetListOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string GroupName { get; set; } = string.Empty; + + public string PartnerId { get; set; } = string.Empty; + + /// + /// 所属合作伙伴名称(列表「Parent Partner」列) + /// + public string PartnerName { get; set; } = string.Empty; + + public bool State { get; set; } + + public DateTime CreationTime { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs new file mode 100644 index 0000000..bceab5b --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs @@ -0,0 +1,21 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Group; + +/// +/// 组织详情 +/// +public class GroupGetOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string GroupName { get; set; } = string.Empty; + + public string PartnerId { get; set; } = string.Empty; + + public string PartnerName { get; set; } = string.Empty; + + public bool State { get; set; } + + public DateTime CreationTime { get; set; } + + public DateTime? LastModificationTime { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs new file mode 100644 index 0000000..563e060 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs @@ -0,0 +1,13 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Group; + +/// +/// 编辑组织入参 +/// +public class GroupUpdateInputVo +{ + public string GroupName { get; set; } = string.Empty; + + public string PartnerId { get; set; } = string.Empty; + + public bool State { get; set; } = true; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs index 5965aec..bd1107f 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs +++ b/美国版/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; public class LabelCreateInputVo { - public string LabelCode { get; set; } = string.Empty; + public string? LabelCode { get; set; } public string LabelName { get; set; } = string.Empty; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs index 986bca2..efdd7fd 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs +++ b/美国版/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 public string CategoryName { get; set; } = string.Empty; + /// + /// 按钮展示文案(为空则默认使用 CategoryName) + /// + public string? DisplayText { get; set; } + + /// + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合) + /// public string? CategoryPhotoUrl { get; set; } public bool State { get; set; } = true; + /// + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl) + /// + public string ButtonAppearance { get; set; } = "TEXT"; + + /// + /// 门店可用范围:ALL / SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; + + /// + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) + /// + public List? LocationIds { get; set; } + public int OrderNum { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs index db2aa7c..c8effdf 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs +++ b/美国版/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 public string CategoryName { get; set; } = string.Empty; + public string? DisplayText { get; set; } + public string? CategoryPhotoUrl { get; set; } public bool State { get; set; } + public string ButtonAppearance { get; set; } = "TEXT"; + + public string AvailabilityType { get; set; } = "ALL"; + public int OrderNum { get; set; } public long NoOfLabels { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs index 242b820..c4a740b 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs +++ b/美国版/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 public string CategoryName { get; set; } = string.Empty; + public string? DisplayText { get; set; } + public string? CategoryPhotoUrl { get; set; } public bool State { get; set; } + public string ButtonAppearance { get; set; } = "TEXT"; + + public string AvailabilityType { get; set; } = "ALL"; + + public List LocationIds { get; set; } = new(); + public int OrderNum { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs index 5b6cf03..643a49d 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs +++ b/美国版/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; namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate; /// -/// 模板元素(对齐你给的 editor JSON:id/type/x/y/width/height/rotation/border/config) +/// 模板元素(对齐 editor JSON:id/type/typeAdd/elementName/x/y/width/height/rotation/border/config 等) /// public class LabelTemplateElementDto { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportCreateInputVo.cs new file mode 100644 index 0000000..e2d70a1 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportCreateInputVo.cs @@ -0,0 +1,11 @@ +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; + +/// +/// 新增门店 Support 联系方式 +/// +public class LocationSupportCreateInputVo +{ + public string SupportPhone { get; set; } = string.Empty; + + public string SupportEmail { get; set; } = string.Empty; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportGetOutputDto.cs new file mode 100644 index 0000000..0dd1801 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportGetOutputDto.cs @@ -0,0 +1,13 @@ +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; + +/// +/// 门店 Support 联系方式详情 +/// +public class LocationSupportGetOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string SupportPhone { get; set; } = string.Empty; + + public string SupportEmail { get; set; } = string.Empty; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportUpdateInputVo.cs new file mode 100644 index 0000000..6e40b87 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportUpdateInputVo.cs @@ -0,0 +1,11 @@ +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; + +/// +/// 编辑门店 Support 联系方式 +/// +public class LocationSupportUpdateInputVo +{ + public string SupportPhone { get; set; } = string.Empty; + + public string SupportEmail { get; set; } = string.Empty; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs new file mode 100644 index 0000000..18d024c --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs @@ -0,0 +1,15 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Partner; + +/// +/// 新建合作伙伴入参 +/// +public class PartnerCreateInputVo +{ + public string PartnerName { get; set; } = string.Empty; + + public string? ContactEmail { get; set; } + + public string? PhoneNumber { get; set; } + + public bool State { get; set; } = true; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs new file mode 100644 index 0000000..50a2387 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs @@ -0,0 +1,19 @@ +using Volo.Abp.Application.Dtos; + +namespace FoodLabeling.Application.Contracts.Dtos.Partner; + +/// +/// 合作伙伴分页查询入参 +/// +public class PartnerGetListInputVo : PagedAndSortedResultRequestDto +{ + /// + /// 模糊搜索(PartnerName / ContactEmail / PhoneNumber) + /// + public string? Keyword { get; set; } + + /// + /// 启用状态(与列表筛选一致) + /// + public bool? State { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs new file mode 100644 index 0000000..42bef41 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs @@ -0,0 +1,19 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Partner; + +/// +/// 合作伙伴列表项 +/// +public class PartnerGetListOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string PartnerName { get; set; } = string.Empty; + + public string? ContactEmail { get; set; } + + public string? PhoneNumber { get; set; } + + public bool State { get; set; } + + public DateTime CreationTime { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs new file mode 100644 index 0000000..f4bf1ac --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs @@ -0,0 +1,21 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Partner; + +/// +/// 合作伙伴详情 +/// +public class PartnerGetOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string PartnerName { get; set; } = string.Empty; + + public string? ContactEmail { get; set; } + + public string? PhoneNumber { get; set; } + + public bool State { get; set; } + + public DateTime CreationTime { get; set; } + + public DateTime? LastModificationTime { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs new file mode 100644 index 0000000..a24c330 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs @@ -0,0 +1,15 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Partner; + +/// +/// 编辑合作伙伴入参 +/// +public class PartnerUpdateInputVo +{ + public string PartnerName { get; set; } = string.Empty; + + public string? ContactEmail { get; set; } + + public string? PhoneNumber { get; set; } + + public bool State { get; set; } = true; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs index 16a4335..3ed7135 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace FoodLabeling.Application.Contracts.Dtos.Product; public class ProductCreateInputVo @@ -11,5 +13,11 @@ public class ProductCreateInputVo public string? ProductImageUrl { get; set; } public bool State { get; set; } = true; + + /// + /// 可选。门店 Id 列表;每个 Id 在 fl_location_product 落一行(同一 fl_product 可对应多门店)。 + /// 不传或空列表则不在本接口写入门店关联(仍可用 product-location 接口维护)。 + /// + public List? LocationIds { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs index 0dd9931..9f45ada 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs +++ b/美国版/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 public string CategoryName { get; set; } = string.Empty; + /// + /// 按钮展示文案(为空则默认使用 CategoryName) + /// + public string? DisplayText { get; set; } + + /// + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合) + /// public string? CategoryPhotoUrl { get; set; } + /// + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl) + /// + public string ButtonAppearance { get; set; } = "TEXT"; + public bool State { get; set; } = true; + /// + /// 门店可用范围:ALL / SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; + + /// + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) + /// + public List? LocationIds { get; set; } + public int OrderNum { get; set; } = 0; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs index ced7d81..51048ce 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs +++ b/美国版/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 public string CategoryName { get; set; } = string.Empty; + public string? DisplayText { get; set; } + public string? CategoryPhotoUrl { get; set; } + public string ButtonAppearance { get; set; } = "TEXT"; + public bool State { get; set; } + public string AvailabilityType { get; set; } = "ALL"; + public int OrderNum { get; set; } public DateTime LastEdited { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs index 3c673a5..2018bc2 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs +++ b/美国版/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 public string CategoryName { get; set; } = string.Empty; + public string? DisplayText { get; set; } + + /// COLOR 色值 / IMAGE 图片 URL / TEXT 可选图 public string? CategoryPhotoUrl { get; set; } + public string ButtonAppearance { get; set; } = "TEXT"; + public bool State { get; set; } + public string AvailabilityType { get; set; } = "ALL"; + + public List LocationIds { get; set; } = new(); + public int OrderNum { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacMenu/RbacMenuGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacMenu/RbacMenuGetListOutputDto.cs index 4a59a70..9bebabf 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacMenu/RbacMenuGetListOutputDto.cs +++ b/美国版/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 public string MenuName { get; set; } = string.Empty; + public string? RouterName { get; set; } + + public string? Router { get; set; } + public string? PermissionCode { get; set; } public int MenuType { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs new file mode 100644 index 0000000..b413c7a --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs @@ -0,0 +1,71 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Reports; + +/// +/// Label Report 聚合结果(卡片 + 图表 + Top 产品表) +/// +public class ReportsLabelReportOutputDto +{ + public ReportsLabelReportSummaryDto Summary { get; set; } = new(); + + /// 按标签分类的打印量(柱状图) + public List LabelsByCategory { get; set; } = new(); + + /// 折线图:默认统计区间内最后 7 个自然日(按日汇总) + public List PrintVolumeTrend { get; set; } = new(); + + /// 用量最高的产品(含占比) + public List MostUsedProducts { get; set; } = new(); +} + +public class ReportsLabelReportSummaryDto +{ + public int TotalLabelsPrinted { get; set; } + + public int TotalLabelsPrintedPrevPeriod { get; set; } + + /// 相对上一同长周期变化率(百分比,如 20.1 表示 +20.1%) + public decimal TotalLabelsPrintedChangeRate { get; set; } + + public string? MostPrintedCategoryName { get; set; } + + public int MostPrintedCategoryCount { get; set; } + + public string? TopProductName { get; set; } + + public int TopProductCount { get; set; } + + public decimal AvgDailyPrints { get; set; } + + public decimal AvgDailyPrintsPrevPeriod { get; set; } + + public decimal AvgDailyPrintsChangeRate { get; set; } +} + +public class ReportsCategoryCountDto +{ + public string? CategoryId { get; set; } + + public string? CategoryName { get; set; } + + public int Count { get; set; } +} + +public class ReportsDailyCountDto +{ + public string Date { get; set; } = string.Empty; + + public int Count { get; set; } +} + +public class ReportsTopProductRowDto +{ + public string? ProductId { get; set; } + + public string? ProductName { get; set; } + + public string? CategoryName { get; set; } + + public int TotalPrinted { get; set; } + + public decimal UsagePercent { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs new file mode 100644 index 0000000..05f14d7 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs @@ -0,0 +1,19 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Reports; + +/// +/// Label Report 统计与导出共用筛选 +/// +public class ReportsLabelReportQueryInputVo +{ + public string? PartnerId { get; set; } + + public string? GroupId { get; set; } + + public string? LocationId { get; set; } + + public DateTime? StartDate { get; set; } + + public DateTime? EndDate { get; set; } + + public string? Keyword { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs new file mode 100644 index 0000000..b1ca2fc --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs @@ -0,0 +1,21 @@ +using Volo.Abp.Application.Dtos; + +namespace FoodLabeling.Application.Contracts.Dtos.Reports; + +/// +/// Web Reports — Print Log 分页查询 +/// +public class ReportsPrintLogGetListInputVo : PagedAndSortedResultRequestDto +{ + public string? PartnerId { get; set; } + + public string? GroupId { get; set; } + + public string? LocationId { get; set; } + + public DateTime? StartDate { get; set; } + + public DateTime? EndDate { get; set; } + + public string? Keyword { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs new file mode 100644 index 0000000..e994ae3 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs @@ -0,0 +1,35 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Reports; + +/// +/// Print Log 列表行(Reports 管理端) +/// +public class ReportsPrintLogListItemDto +{ + /// 打印任务 Id(fl_label_print_task.Id),重打时使用 + public string TaskId { get; set; } = string.Empty; + + /// 标签编码(展示为 Label ID) + public string LabelCode { get; set; } = string.Empty; + + public string ProductName { get; set; } = "无"; + + /// 分类展示名(优先产品分类,否则标签分类) + public string CategoryName { get; set; } = "无"; + + /// 模板展示(尺寸 + 模板名) + public string TemplateText { get; set; } = "无"; + + public DateTime PrintedAt { get; set; } + + /// 打印人姓名 + public string PrintedByName { get; set; } = "无"; + + /// 门店展示:名称 (编码) + public string LocationText { get; set; } = "无"; + + /// 门店 Id(重打校验用) + public string? LocationId { get; set; } + + /// 从 PrintInputJson 尽力解析的保质期文本;无则「无」 + public string ExpiryDateText { get; set; } = "无"; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs new file mode 100644 index 0000000..d04fe9e --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs @@ -0,0 +1,16 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; + +/// +/// App 修改密码入参 +/// +public class UsAppChangePasswordInputVo +{ + /// 当前密码 + public string CurrentPassword { get; set; } = string.Empty; + + /// 新密码 + public string NewPassword { get; set; } = string.Empty; + + /// 确认新密码 + public string ConfirmNewPassword { get; set; } = string.Empty; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs new file mode 100644 index 0000000..d684a8b --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs @@ -0,0 +1,28 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; + +/// +/// App「Location」门店详情(与原型字段对齐) +/// +public class UsAppLocationDetailOutputDto +{ + /// 门店主键(Guid 字符串) + public string LocationId { get; set; } = string.Empty; + + /// 门店名称 + public string LocationName { get; set; } = string.Empty; + + /// 完整地址(街道、城市、州、邮编拼接;无则为「无」) + public string FullAddress { get; set; } = string.Empty; + + /// 门店电话(来自 location.Phone;空为「无」) + public string StorePhone { get; set; } = string.Empty; + + /// 营业时间;当前库无字段,固定返回「无」直至业务落库 + public string OperatingHours { get; set; } = string.Empty; + + /// 店长姓名;优先取绑定本店且角色名/编码含 manager 的用户 + public string ManagerName { get; set; } = string.Empty; + + /// 店长电话;同上用户 User.Phone 格式化;无则为「无」 + public string ManagerPhone { get; set; } = string.Empty; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs new file mode 100644 index 0000000..107c365 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs @@ -0,0 +1,25 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; + +/// +/// App「我的资料」展示数据(My Profile) +/// +public class UsAppMyProfileOutputDto +{ + /// 全名(Name,无则 Nick,再无则 UserName) + public string FullName { get; set; } = string.Empty; + + /// 邮箱 + public string Email { get; set; } = "无"; + + /// 电话展示(如 +1 (555) 123-4567);无则「无」 + public string Phone { get; set; } = "无"; + + /// 员工号/登录名(当前使用 User.UserName,可与业务约定为工号) + public string EmployeeId { get; set; } = string.Empty; + + /// 角色展示名(多角色英文逗号拼接,按角色 OrderNum) + public string RoleDisplay { get; set; } = "无"; + + /// 主角色编码(第一个角色 RoleCode,供前端样式) + public string? PrimaryRoleCode { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs index 4e2ce5d..dc93a0b 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs +++ b/美国版/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 public string? CategoryPhotoUrl { get; set; } + /// 按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl) + public string ButtonAppearance { get; set; } = "TEXT"; + public int OrderNum { get; set; } public List ProductCategories { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs index 0977cdf..24d9679 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs +++ b/美国版/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 public JsonElement? PrintInputJson { get; set; } /// - /// 客户端幂等请求 Id(可选);重复相同值时由服务端决定是否直接返回首次结果(见接口文档)。 - /// - public string? ClientRequestId { get; set; } - - /// /// 打印机Id(可选,若业务需要追踪) /// public string? PrinterId { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs index fd93dae..ddf43ea 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs +++ b/美国版/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 /// 分类显示名;空为「无」 public string Name { get; set; } = string.Empty; + /// 按钮展示文案;为空时客户端可回退使用 Name + public string? DisplayText { get; set; } + + /// 按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl) + public string ButtonAppearance { get; set; } = "TEXT"; + + /// 门店可用范围:ALL / SPECIFIED(本树已按当前门店过滤) + public string AvailabilityType { get; set; } = "ALL"; + + /// 排序号(来自 fl_product_category;未归类为较大值以排在后) + public int OrderNum { get; set; } + public int ItemCount { get; set; } public List Products { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs new file mode 100644 index 0000000..a033cd9 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs @@ -0,0 +1,33 @@ +using FoodLabeling.Application.Contracts.Dtos.AuthSession; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// 当前登录会话:菜单权限与退出(美国版 Web 管理端) +/// +public interface IAuthSessionAppService : IApplicationService +{ + /// + /// 获取当前登录用户的角色编码、权限码与可见菜单树 + /// + /// + /// 与框架 UserManager.GetInfoAsync 一致;用户名为 admin 时返回全部未删除菜单(与 AccountService.GetVue3Router 行为对齐)。 + /// + /// 用户简要信息、权限码与菜单树 + /// 成功 + /// 未登录或令牌无效 + /// 服务器错误 + Task GetMyMenusAsync(); + + /// + /// 退出登录:清除服务端用户信息缓存(JWT 仍由前端丢弃) + /// + /// + /// 与框架 AccountService.PostLogout 一致;未登录时返回 false。 + /// + /// 是否执行了缓存清理(已登录为 true) + /// 成功 + /// 服务器错误 + Task LogoutAsync(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs new file mode 100644 index 0000000..4e825da --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs @@ -0,0 +1,15 @@ +using FoodLabeling.Application.Contracts.Dtos.Dashboard; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// Dashboard 统计接口(美国版) +/// +public interface IDashboardAppService : IApplicationService +{ + /// + /// 获取 Dashboard 总览统计(卡片 + 周趋势 + 分类分布 + Recent Labels 最近打印) + /// + Task GetOverviewAsync(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs new file mode 100644 index 0000000..8bd2957 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs @@ -0,0 +1,44 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Group; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// 组织(Group)管理接口(fl_group) +/// +public interface IGroupAppService : IApplicationService +{ + /// + /// 组织分页列表(与导出使用相同筛选条件) + /// + /// 分页与筛选;SkipCount 为页码(从 1 起) + Task> GetListAsync(GroupGetListInputVo input); + + /// + /// 组织详情 + /// + Task GetAsync(string id); + + /// + /// 新增组织 + /// + Task CreateAsync(GroupCreateInputVo input); + + /// + /// 编辑组织 + /// + Task UpdateAsync(string id, GroupUpdateInputVo input); + + /// + /// 删除组织(逻辑删除) + /// + Task DeleteAsync(string id); + + /// + /// 按列表相同筛选条件导出组织为 PDF(上限 5000 条) + /// + Task ExportPdfAsync(GroupGetListInputVo input); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationSupportAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationSupportAppService.cs new file mode 100644 index 0000000..73f3a9d --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationSupportAppService.cs @@ -0,0 +1,28 @@ +using FoodLabeling.Application.Contracts.Dtos.LocationSupport; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// 全局 Support 联系方式(全平台共用;Web 可增改查,App 仅可查) +/// +public interface ILocationSupportAppService : IApplicationService +{ + /// + /// 查询全局 Support 联系方式(已登录即可;App / Web 共用) + /// + Task GetSupportAsync(); + + /// + /// 新增全局 Support 联系方式(系统仅允许一条;Web 管理端) + /// + /// 联系方式 + Task CreateAsync(LocationSupportCreateInputVo input); + + /// + /// 编辑全局 Support 联系方式(Web 管理端) + /// + /// 联系方式主键 + /// 联系方式 + Task UpdateAsync(string id, LocationSupportUpdateInputVo input); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs new file mode 100644 index 0000000..47ee7e1 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs @@ -0,0 +1,87 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Partner; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// 合作伙伴管理接口(fl_partner) +/// +public interface IPartnerAppService : IApplicationService +{ + /// + /// 合作伙伴分页列表(与导出使用相同筛选条件) + /// + /// 分页与筛选;SkipCount 为页码(从 1 起) + /// 分页数据 + /// 成功 + /// 参数错误 + /// 服务器错误 + Task> GetListAsync(PartnerGetListInputVo input); + + /// + /// 合作伙伴详情 + /// + /// 主键 Id + /// 详情 + /// 成功 + /// Id 无效 + /// 服务器错误 + Task GetAsync(string id); + + /// + /// 新增合作伙伴 + /// + /// 名称、邮箱、电话、启用状态 + /// 新建后的详情 + /// + /// 示例请求: + /// ```json + /// { + /// "partnerName": "Global Foods Inc.", + /// "contactEmail": "admin@globalfoods.com", + /// "phoneNumber": "+1 (555) 100-2000", + /// "state": true + /// } + /// ``` + /// + /// 成功 + /// 校验失败 + /// 服务器错误 + Task CreateAsync(PartnerCreateInputVo input); + + /// + /// 编辑合作伙伴 + /// + /// 主键 Id + /// 名称、邮箱、电话、启用状态 + /// 更新后的详情 + /// 成功 + /// 校验失败或记录不存在 + /// 服务器错误 + Task UpdateAsync(string id, PartnerUpdateInputVo input); + + /// + /// 删除合作伙伴(逻辑删除) + /// + /// 主键 Id + /// 成功 + /// Id 无效或记录不存在 + /// 服务器错误 + Task DeleteAsync(string id); + + /// + /// 按当前列表筛选条件批量导出合作伙伴为 PDF(不分页,上限 5000 条) + /// + /// 与列表相同的 Keyword、State;分页字段忽略 + /// PDF 文件流 + /// + /// 筛选条件需与 一致,便于统计与导出数据对齐。 + /// + /// 成功返回 application/pdf + /// 参数错误 + /// 服务器错误 + Task ExportPdfAsync(PartnerGetListInputVo input); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs index c0f0cbd..4193329 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs +++ b/美国版/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 /// /// 新增产品 /// + /// + /// 若 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 + /// Task CreateAsync(ProductCreateInputVo input); /// /// 编辑产品 /// + /// + /// 当请求体包含 属性时,按该列表整表替换本产品在各门店的关联; + /// 不传该属性则不改门店关联(兼容仅改名称/分类等调用)。 + /// Task UpdateAsync(string id, ProductUpdateInputVo input); /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs new file mode 100644 index 0000000..e8ced4a --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs @@ -0,0 +1,38 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Reports; +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// Reports(Print Log / Label Report)管理端接口 +/// +public interface IReportsAppService : IApplicationService +{ + /// + /// Print Log 分页列表;角色 admin 可查全部,否则仅当前用户打印记录。 + /// + Task> GetPrintLogListAsync(ReportsPrintLogGetListInputVo input); + + /// + /// Print Log 导出 PDF(筛选与列表一致,最多 5000 条) + /// + Task ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input); + + /// + /// 根据历史任务重打(与 App 入参一致);admin 可重打任意用户任务,否则仅本人任务。 + /// + Task ReprintPrintLogAsync(UsAppLabelReprintInputVo input); + + /// + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);admin 统计全部,否则仅当前用户。 + /// + Task GetLabelReportAsync(ReportsLabelReportQueryInputVo input); + + /// + /// Label Report 导出 PDF + /// + Task ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs index 1b439e0..acfdbf1 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs +++ b/美国版/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 /// 获取当前登录账号已绑定的门店(用于切换门店等场景) /// Task> GetMyLocationsAsync(); + + /// + /// 获取当前登录用户资料(My Profile:姓名、邮箱、电话、员工号、角色) + /// + /// 资料 DTO + /// 成功 + /// 未登录或用户不存在 + /// 服务器错误 + Task GetMyProfileAsync(); + + /// + /// 当前登录用户修改密码(校验原密码与复杂度规则) + /// + /// 当前密码、新密码、确认密码 + /// + /// 新密码需满足:至少 8 位;含大写与小写字母;至少 1 位数字;至少 1 个非字母数字特殊字符。 + /// + /// 成功 + /// 参数或校验失败 + /// 服务器错误 + Task ChangePasswordAsync(UsAppChangePasswordInputVo input); + + /// + /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) + /// + /// 门店 Guid 字符串 + /// 店名、地址、电话、营业时间占位、店长信息 + /// 成功 + /// 参数非法、未绑定或无权限 + /// 服务器错误 + Task GetLocationDetailAsync(string locationId); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs index ccde16d..781adcd 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs @@ -1,6 +1,5 @@ using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; -using FoodLabeling.Application.Contracts.Dtos.Common; using Volo.Abp.Application.Services; namespace FoodLabeling.Application.Contracts.IServices; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs new file mode 100644 index 0000000..2727138 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs @@ -0,0 +1,13 @@ +namespace FoodLabeling.Application.Contracts; + +/// +/// 美国版 App JWT 自定义声明(用于与 Web 管理端 Token 区分能力) +/// +public static class UsAppJwtClaims +{ + /// 声明类型:客户端种类 + public const string ClientKind = "client_kind"; + + /// 美国版移动端 App + public const string ClientKindUsApp = "us-app"; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj index 9c45fa3..44933dd 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj @@ -3,6 +3,7 @@ + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/CategoryAppearanceStorageHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/CategoryAppearanceStorageHelper.cs new file mode 100644 index 0000000..7faa3ec --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/CategoryAppearanceStorageHelper.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using Volo.Abp; + +namespace FoodLabeling.Application.Helpers; + +/// +/// 将标签/产品类别的按钮外观与展示字段按「JSON 字符串」落库;兼容历史单行 TEXT/COLOR/IMAGE。 +/// +public static class CategoryAppearanceStorageHelper +{ + /// 未传按钮外观时的默认 JSON(与前端数组语义一致)。 + public const string DefaultButtonAppearanceJson = """["TEXT"]"""; + + /// + /// 规范化 / + /// 产品类别同名字段:落库为合法 JSON 文本,不做整串 ToUpper(避免破坏 JSON)。 + /// + public static string NormalizeButtonAppearanceForStorage(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return DefaultButtonAppearanceJson; + } + + var t = raw.Trim(); + var legacy = t.ToUpperInvariant(); + if (legacy is "TEXT" or "COLOR" or "IMAGE") + { + return JsonSerializer.Serialize(new[] { legacy }); + } + + try + { + using var _ = JsonDocument.Parse(t); + return t; + } + catch (JsonException) + { + throw new UserFriendlyException("按钮外观格式不正确,须为合法 JSON(或兼容旧的 TEXT/COLOR/IMAGE)"); + } + } + + /// + /// 规范化 / + /// 产品类别同名字段:已是 JSON 则原样落库;否则将整段文本序列化为 JSON 字符串(兼容历史单行色值/URL)。 + /// + public static string? NormalizeCategoryPhotoUrlForStorage(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return null; + } + + var t = raw.Trim(); + try + { + using var _ = JsonDocument.Parse(t); + return t; + } + catch (JsonException) + { + return JsonSerializer.Serialize(t); + } + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs new file mode 100644 index 0000000..01a205a --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs @@ -0,0 +1,31 @@ +using Volo.Abp.Users; + +namespace FoodLabeling.Application.Helpers; + +/// +/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致) +/// +public static class ReportsRoleHelper +{ + /// + /// 是否为管理员:任一角色码等于 admin(忽略大小写)则视为可查看全部打印数据。 + /// + public static bool IsAdminRole(ICurrentUser currentUser) + { + if (currentUser.Roles is null) + { + return false; + } + + foreach (var r in currentUser.Roles) + { + if (!string.IsNullOrWhiteSpace(r) && + string.Equals(r.Trim(), "admin", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs new file mode 100644 index 0000000..7507726 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs @@ -0,0 +1,213 @@ +using FoodLabeling.Application.Contracts.Dtos.AuthSession; +using FoodLabeling.Application.Contracts.IServices; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Caching; +using FoodLabeling.Application.Services.DbModels; +using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.Rbac.Domain.Shared.Caches; +using Yi.Framework.Rbac.Domain.Shared.Consts; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// 当前登录会话:菜单权限与退出 +/// +[Authorize] +public class AuthSessionAppService : ApplicationService, IAuthSessionAppService +{ + private readonly IDistributedCache _userCache; + private readonly ISqlSugarDbContext _dbContext; + private readonly ISqlSugarRepository _userRepository; + + public AuthSessionAppService( + ISqlSugarDbContext dbContext, + ISqlSugarRepository userRepository, + IDistributedCache userCache) + { + _dbContext = dbContext; + _userRepository = userRepository; + _userCache = userCache; + } + + /// + public virtual async Task GetMyMenusAsync() + { + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + // 避免走 UserManager.GetInfoAsync -> UserRepository.GetUserAllInfoAsync 的导航加载 + // 这里直接按 UserRole/RoleMenu/Menu 表关联查询当前用户可见菜单与权限码 + var userId = CurrentUser.Id.Value; + var user = await _userRepository.GetByIdAsync(userId); + if (user is null || user.IsDeleted) + { + throw new UserFriendlyException("用户不存在"); + } + + List menus; + if (UserConst.Admin.Equals(user.UserName)) + { + // MenuAggregateRoot(ParentId 为 Guid) 无法兼容 menu.ParentId=0/字符串:这里统一用 MenuDbEntity + menus = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.IsDeleted == false) + .ToListAsync(); + } + else + { + var roleIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.UserId == userId) + .Select(x => x.RoleId) + .ToListAsync(); + + var roleIdStrs = roleIds.Select(x => x.ToString()).Distinct().ToList(); + if (roleIdStrs.Count == 0) + { + menus = new List(); + } + else + { + var menuIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => roleIdStrs.Contains(x.RoleId)) + .Select(x => x.MenuId) + .Distinct() + .ToListAsync(); + + menus = menuIds.Count == 0 + ? new List() + : await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id)) + .ToListAsync(); + } + } + + var menuNodes = menus + .Select(MapToNode) + .OrderByDescending(x => x.OrderNum) + .ThenBy(x => x.MenuName) + .ToList(); + + // 注意:查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤, + // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。 + // 这里直接使用 JWT 中的角色码(CurrentUser.Roles)返回,避免触发过滤器。 + var roleCodes = CurrentUser.Roles?.ToList() ?? new List(); + + var permissionCodes = menuNodes + .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode)) + .Select(x => x.PermissionCode!.Trim()) + .Distinct() + .OrderBy(x => x) + .ToList(); + + return new CurrentUserMenuPermissionsOutputDto + { + User = new CurrentUserBriefDto + { + Id = user.Id, + UserName = user.UserName, + Nick = user.Nick, + Email = user.Email, + Icon = user.Icon + }, + RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(), + PermissionCodes = permissionCodes, + Menus = BuildMenuTree(menuNodes) + }; + } + + /// + [HttpPost] + public virtual async Task LogoutAsync() + { + if (!CurrentUser.Id.HasValue) + { + return false; + } + + await _userCache.RemoveAsync(new UserInfoCacheKey(CurrentUser.Id.Value)); + return true; + } + + private static List BuildMenuTree(List flat) + { + var nodes = flat + .GroupBy(x => x.Id) + .Select(g => g.First()) + .ToList(); + var byId = nodes.ToDictionary(n => n.Id, n => n); + + foreach (var n in nodes) + { + n.Children = new List(); + } + + var roots = new List(); + foreach (var n in nodes) + { + var pid = string.IsNullOrWhiteSpace(n.ParentId) ? "0" : n.ParentId.Trim(); + if (pid == "0" || pid == "00000000-0000-0000-0000-000000000000") + { + roots.Add(n); + continue; + } + + if (byId.TryGetValue(pid, out var parent)) + { + parent.Children.Add(n); + } + else + { + roots.Add(n); + } + } + + SortMenuTree(roots); + return roots; + } + + private static CurrentUserMenuNodeDto MapToNode(MenuDbEntity m) + { + return new CurrentUserMenuNodeDto + { + Id = m.Id, + ParentId = string.IsNullOrWhiteSpace(m.ParentId) ? "0" : m.ParentId.Trim(), + MenuName = m.MenuName ?? string.Empty, + RouterName = m.RouterName, + Router = m.Router, + PermissionCode = m.PermissionCode, + MenuType = m.MenuType, + MenuSource = m.MenuSource, + OrderNum = m.OrderNum, + State = m.State, + MenuIcon = m.MenuIcon, + Component = m.Component, + IsLink = m.IsLink, + IsCache = m.IsCache, + IsShow = m.IsShow, + Query = m.Query, + Remark = m.Remark + }; + } + + private static void SortMenuTree(List level) + { + level.Sort((a, b) => + { + var o = b.OrderNum.CompareTo(a.OrderNum); + return o != 0 ? o : string.Compare(a.MenuName, b.MenuName, StringComparison.Ordinal); + }); + + foreach (var n in level) + { + if (n.Children.Count > 0) + { + SortMenuTree(n.Children); + } + } + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs new file mode 100644 index 0000000..94470a5 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs @@ -0,0 +1,339 @@ +using System.Globalization; +using System.Text.Json; +using FoodLabeling.Application.Contracts.Dtos.Dashboard; +using FoodLabeling.Application.Contracts.IServices; +using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; +using SqlSugar; +using Volo.Abp.Application.Services; +using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// Dashboard 统计服务(美国版) +/// +public class DashboardAppService : ApplicationService, IDashboardAppService +{ + private readonly ISqlSugarDbContext _dbContext; + private readonly ISqlSugarRepository _locationRepository; + private readonly ISqlSugarRepository _userRepository; + + public DashboardAppService( + ISqlSugarDbContext dbContext, + ISqlSugarRepository locationRepository, + ISqlSugarRepository userRepository) + { + _dbContext = dbContext; + _locationRepository = locationRepository; + _userRepository = userRepository; + } + + /// + public async Task GetOverviewAsync() + { + var now = DateTime.Now; + var todayStart = now.Date; + var tomorrowStart = todayStart.AddDays(1); + var yesterdayStart = todayStart.AddDays(-1); + var weekStart = todayStart.AddDays(-6); + var prevWeekStart = todayStart.AddDays(-13); + + var printedToday = await _dbContext.SqlSugarClient.Queryable() + .CountAsync(x => x.CreationTime >= todayStart && x.CreationTime < tomorrowStart); + + var printedYesterday = await _dbContext.SqlSugarClient.Queryable() + .CountAsync(x => x.CreationTime >= yesterdayStart && x.CreationTime < todayStart); + + var activeTemplates = await _dbContext.SqlSugarClient.Queryable() + .CountAsync(x => !x.IsDeleted && x.State); + var activeTemplatesPrevWeek = await _dbContext.SqlSugarClient.Queryable() + .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart); + + var activeUsers = await _userRepository._DbQueryable + .CountAsync(x => !x.IsDeleted && x.State); + var activeUsersPrevWeek = await _userRepository._DbQueryable + .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart); + + var locations = await _locationRepository._DbQueryable + .CountAsync(x => !x.IsDeleted); + var locationsPrevWeek = await _locationRepository._DbQueryable + .CountAsync(x => !x.IsDeleted && x.CreationTime < weekStart); + + var people = await _userRepository._DbQueryable + .CountAsync(x => !x.IsDeleted); + var peoplePrevWeek = await _userRepository._DbQueryable + .CountAsync(x => !x.IsDeleted && x.CreationTime < weekStart); + + var products = await _dbContext.SqlSugarClient.Queryable() + .CountAsync(x => !x.IsDeleted); + // fl_product 当前实体未映射 CreationTime,无法按时间切分对比,先回退为同口径总量对比 + var productsPrevWeek = products; + + var weeklyPrintRaw = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.CreationTime >= weekStart && x.CreationTime < tomorrowStart) + .Select(x => x.CreationTime) + .ToListAsync(); + + var weeklyDict = weeklyPrintRaw + .GroupBy(x => x.Date) + .ToDictionary(g => g.Key, g => g.Count()); + + var weeklyTrend = Enumerable.Range(0, 7) + .Select(i => + { + var d = weekStart.AddDays(i).Date; + return new DashboardDailyTrendPointDto + { + Date = d.ToString("yyyy-MM-dd"), + Value = weeklyDict.TryGetValue(d, out var v) ? v : 0 + }; + }) + .ToList(); + + var categories = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && x.State) + .ToListAsync(); + + var labelCategoryIds = categories.Select(x => x.Id).ToList(); + var labelRows = labelCategoryIds.Count == 0 + ? new List() + : await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && x.LabelCategoryId != null && labelCategoryIds.Contains(x.LabelCategoryId)) + .Select(x => x.LabelCategoryId) + .ToListAsync(); + + var labelCountByCategory = labelRows + .Where(x => !string.IsNullOrWhiteSpace(x)) + .GroupBy(x => x!) + .ToDictionary(g => g.Key, g => g.Count()); + + var categoryDistributionTotal = labelCountByCategory.Values.Sum(); + var categoryDistribution = categories + .Select(c => + { + var count = labelCountByCategory.TryGetValue(c.Id, out var v) ? v : 0; + var ratio = categoryDistributionTotal == 0 + ? 0m + : Math.Round(count * 100m / categoryDistributionTotal, 2); + return new DashboardCategoryDistributionDto + { + CategoryId = c.Id, + CategoryName = c.CategoryName, + Count = count, + Ratio = ratio + }; + }) + .Where(x => x.Count > 0) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.CategoryName) + .ToList(); + + const int recentLabelsTake = 10; + var recentRaw = await _dbContext.SqlSugarClient.Queryable() + .LeftJoin((t, l) => t.LabelId == l.Id) + .LeftJoin((t, l, p) => t.ProductId == p.Id) + .LeftJoin((t, l, p, tpl) => t.TemplateId == tpl.Id) + .OrderBy((t, l, p, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) + .Take(recentLabelsTake) + .Select((t, l, p, tpl) => new + { + t.Id, + LabelCode = l.LabelCode, + LabelName = l.LabelName, + ProductName = p.ProductName, + tpl.Width, + tpl.Height, + tpl.Unit, + t.PrintInputJson, + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), + t.CreatedBy + }) + .ToListAsync(); + + var recentUserIds = recentRaw + .Select(x => x.CreatedBy) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x!.Trim()) + .Distinct() + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .ToList(); + + var recentUserMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (recentUserIds.Count > 0) + { + var users = await _userRepository._DbQueryable + .Where(u => !u.IsDeleted && recentUserIds.Contains(u.Id)) + .Select(u => new { u.Id, u.Name, u.UserName }) + .ToListAsync(); + foreach (var u in users) + { + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); + recentUserMap[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; + } + } + + var recentLabels = recentRaw.Select(x => + { + var displayName = !string.IsNullOrWhiteSpace(x.ProductName) + ? x.ProductName.Trim() + : (string.IsNullOrWhiteSpace(x.LabelName) ? "无" : x.LabelName.Trim()); + var printedAt = x.PrintedAt ?? DateTime.MinValue; + var status = ResolveRecentLabelStatus(x.PrintInputJson); + return new DashboardRecentLabelItemDto + { + TaskId = x.Id, + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), + DisplayName = displayName, + PrintedByUserId = x.CreatedBy?.Trim(), + PrintedByName = ResolveRecentUserName(recentUserMap, x.CreatedBy), + PrintedAt = printedAt, + Status = status, + LabelTypeBadge = FormatTemplateBadge(x.Width, x.Height, x.Unit) + }; + }).ToList(); + + var labelsPrintedTodayCard = BuildMetricCard("labelsPrintedToday", "Labels Printed Today", printedToday, printedYesterday); + var activeTemplatesCard = BuildMetricCard("activeTemplates", "Active Templates", activeTemplates, activeTemplatesPrevWeek); + var activeUsersCard = BuildMetricCard("activeUsers", "Active Users", activeUsers, activeUsersPrevWeek); + var locationsCard = BuildMetricCard("locations", "Locations", locations, locationsPrevWeek); + var peopleCard = BuildMetricCard("people", "People", people, peoplePrevWeek); + var productsCard = BuildMetricCard("products", "Products", products, productsPrevWeek); + + var output = new DashboardOverviewOutputDto + { + LabelsPrintedToday = labelsPrintedTodayCard, + ActiveTemplates = activeTemplatesCard, + ActiveUsers = activeUsersCard, + Locations = locationsCard, + People = peopleCard, + Products = productsCard, + MetricCards = new List + { + labelsPrintedTodayCard, + activeTemplatesCard, + activeUsersCard, + locationsCard, + peopleCard, + productsCard + }, + WeeklyPrintVolume = weeklyTrend, + CategoryDistribution = categoryDistribution, + CategoryDistributionTotal = categoryDistributionTotal, + ByCategory = categoryDistribution, + ByCategoryTotal = categoryDistributionTotal, + RecentLabels = recentLabels, + GeneratedAt = now + }; + + return output; + } + + private static string ResolveRecentUserName(Dictionary map, string? createdBy) + { + if (string.IsNullOrWhiteSpace(createdBy)) + { + return "无"; + } + + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; + } + + /// + /// 依据 PrintInputJson 中的保质期字段与「当前日期」比较得到 active/expired。 + /// + private static string ResolveRecentLabelStatus(string? printInputJson) + { + if (!TryParseExpiryDate(printInputJson, out var expiryDate)) + { + return "active"; + } + + var today = DateTime.Now.Date; + return expiryDate.Date < today ? "expired" : "active"; + } + + private static bool TryParseExpiryDate(string? printInputJson, out DateTime expiryDate) + { + expiryDate = default; + if (string.IsNullOrWhiteSpace(printInputJson)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(printInputJson); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + foreach (var prop in doc.RootElement.EnumerateObject()) + { + var key = prop.Name.Trim(); + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var v = prop.Value; + if (v.ValueKind == JsonValueKind.String) + { + var s = v.GetString(); + if (!string.IsNullOrWhiteSpace(s) && + DateTime.TryParse(s, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces, out var dt)) + { + expiryDate = dt; + return true; + } + } + else if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var unix)) + { + expiryDate = DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime; + return true; + } + } + } + catch + { + return false; + } + + return false; + } + + private static string FormatTemplateBadge(decimal w, decimal h, string? unit) + { + var u = (unit ?? "inch").Trim().ToLowerInvariant(); + var ws = w.ToString(CultureInfo.InvariantCulture); + var hs = h.ToString(CultureInfo.InvariantCulture); + return u is "inch" or "in" + ? $"{ws}\"x{hs}\"" + : $"{ws}x{hs}{u}"; + } + + private static DashboardMetricCardDto BuildMetricCard(string key, string title, int value, int previousValue) + { + var changeValue = value - previousValue; + var changeRate = previousValue <= 0 + ? (value > 0 ? 100m : 0m) + : Math.Round(changeValue * 100m / previousValue, 2); + + return new DashboardMetricCardDto + { + Key = key, + Title = title, + Value = value, + PreviousValue = previousValue, + ChangeValue = changeValue, + ChangeRate = changeRate + }; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs new file mode 100644 index 0000000..912d853 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs @@ -0,0 +1,38 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 组织/分组(Account Management / Group,表 fl_group) +/// +[SugarTable("fl_group")] +public class FlGroupDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } + + public string? LastModifierId { get; set; } + + public DateTime? LastModificationTime { get; set; } + + /// + /// 组织名称(Group Name) + /// + public string GroupName { get; set; } = string.Empty; + + /// + /// 所属合作伙伴 Id(fl_partner.Id) + /// + public string PartnerId { get; set; } = string.Empty; + + /// + /// 是否启用(对应 UI Active) + /// + public bool State { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs index d70b724..b89ab82 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs @@ -24,10 +24,28 @@ public class FlLabelCategoryDbEntity public string CategoryName { get; set; } = string.Empty; + /// + /// 按钮展示文案(为空则默认使用 CategoryName) + /// + public string? DisplayText { get; set; } + + /// + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值(如 #409EFF);IMAGE 存图片 URL(与 ButtonAppearance 配合) + /// public string? CategoryPhotoUrl { get; set; } public int OrderNum { get; set; } public bool State { get; set; } + + /// + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl) + /// + public string ButtonAppearance { get; set; } = "TEXT"; + + /// + /// 门店可用范围:ALL / SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryLocationDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryLocationDbEntity.cs new file mode 100644 index 0000000..ae9b896 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryLocationDbEntity.cs @@ -0,0 +1,22 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 标签分类可用门店关联(对应表:fl_label_category_location) +/// +[SugarTable("fl_label_category_location")] +public class FlLabelCategoryLocationDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public string CategoryId { get; set; } = string.Empty; + + public string LocationId { get; set; } = string.Empty; + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs index 3199335..23f6f20 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs @@ -20,7 +20,7 @@ public class FlLabelDbEntity public string ConcurrencyStamp { get; set; } = string.Empty; - public string LabelCode { get; set; } = string.Empty; + public string? LabelCode { get; set; } public string LabelName { get; set; } = string.Empty; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationSupportDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationSupportDbEntity.cs new file mode 100644 index 0000000..746ba5a --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationSupportDbEntity.cs @@ -0,0 +1,27 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 门店 Support 联系方式(每个门店仅一条,对 App Support 页展示) +/// +[SugarTable("fl_location_support")] +public class FlLocationSupportDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } + + public string? LastModifierId { get; set; } + + public DateTime? LastModificationTime { get; set; } + + public string SupportPhone { get; set; } = string.Empty; + + public string SupportEmail { get; set; } = string.Empty; +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs new file mode 100644 index 0000000..606d2c6 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs @@ -0,0 +1,43 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 合作伙伴主数据(Account Management / Partner,表 fl_partner) +/// +[SugarTable("fl_partner")] +public class FlPartnerDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } + + public string? LastModifierId { get; set; } + + public DateTime? LastModificationTime { get; set; } + + /// + /// 合作伙伴名称(公司名) + /// + public string PartnerName { get; set; } = string.Empty; + + /// + /// 联系邮箱 + /// + public string? ContactEmail { get; set; } + + /// + /// 电话 + /// + public string? PhoneNumber { get; set; } + + /// + /// 是否启用(对应 UI Active) + /// + public bool State { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs index 1f51a75..2852756 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs @@ -24,10 +24,28 @@ public class FlProductCategoryDbEntity public string CategoryName { get; set; } = string.Empty; + /// + /// 按钮展示文案(为空则默认使用 CategoryName) + /// + public string? DisplayText { get; set; } + + /// + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值;IMAGE 存图片 URL(与 ButtonAppearance 配合) + /// public string? CategoryPhotoUrl { get; set; } + /// + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl) + /// + public string ButtonAppearance { get; set; } = "TEXT"; + public bool State { get; set; } + /// + /// 门店可用范围:ALL / SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; + public int OrderNum { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryLocationDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryLocationDbEntity.cs new file mode 100644 index 0000000..c6a6795 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryLocationDbEntity.cs @@ -0,0 +1,22 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 产品类别可用门店关联(对应表:fl_product_category_location) +/// +[SugarTable("fl_product_category_location")] +public class FlProductCategoryLocationDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public string CategoryId { get; set; } = string.Empty; + + public string LocationId { get; set; } = string.Empty; + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs new file mode 100644 index 0000000..36d7c75 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs @@ -0,0 +1,356 @@ +using FoodLabeling.Application.Helpers; +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Group; +using FoodLabeling.Application.Contracts.IServices; +using FoodLabeling.Application.Services.DbModels; +using Microsoft.AspNetCore.Mvc; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Guids; +using Volo.Abp.Uow; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// 组织(Group)管理(fl_group) +/// +public class GroupAppService : ApplicationService, IGroupAppService +{ + private const int ExportPdfMaxRows = 5000; + + private readonly ISqlSugarDbContext _dbContext; + private readonly IGuidGenerator _guidGenerator; + + public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) + { + _dbContext = dbContext; + _guidGenerator = guidGenerator; + } + + /// + public async Task> GetListAsync(GroupGetListInputVo input) + { + RefAsync total = 0; + var query = BuildGroupJoinedQuery(input); + var projected = query.Select((g, p) => new GroupGetListOutputDto + { + Id = g.Id, + GroupName = g.GroupName, + PartnerId = g.PartnerId, + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), + State = g.State, + CreationTime = g.CreationTime + }); + + var items = await projected.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); + } + + /// + public async Task GetAsync(string id) + { + var groupId = id?.Trim(); + if (string.IsNullOrWhiteSpace(groupId)) + { + throw new UserFriendlyException("组织Id不能为空"); + } + + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); + if (entity is null) + { + throw new UserFriendlyException("组织不存在"); + } + + var partnerName = await ResolvePartnerNameAsync(entity.PartnerId); + return MapDetail(entity, partnerName); + } + + /// + [UnitOfWork] + public async Task CreateAsync(GroupCreateInputVo input) + { + var name = input.GroupName?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new UserFriendlyException("组织名称不能为空"); + } + + var partnerId = input.PartnerId?.Trim(); + if (string.IsNullOrWhiteSpace(partnerId)) + { + throw new UserFriendlyException("请选择所属合作伙伴"); + } + + await EnsurePartnerExistsAsync(partnerId); + + var now = Clock.Now; + var entity = new FlGroupDbEntity + { + Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + GroupName = name, + PartnerId = partnerId, + State = input.State, + CreationTime = now, + CreatorId = CurrentUser?.Id?.ToString(), + LastModificationTime = now, + LastModifierId = CurrentUser?.Id?.ToString() + }; + + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + return await GetAsync(entity.Id); + } + + /// + [UnitOfWork] + public async Task UpdateAsync(string id, GroupUpdateInputVo input) + { + var groupId = id?.Trim(); + if (string.IsNullOrWhiteSpace(groupId)) + { + throw new UserFriendlyException("组织Id不能为空"); + } + + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); + if (entity is null) + { + throw new UserFriendlyException("组织不存在"); + } + + var name = input.GroupName?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new UserFriendlyException("组织名称不能为空"); + } + + var partnerId = input.PartnerId?.Trim(); + if (string.IsNullOrWhiteSpace(partnerId)) + { + throw new UserFriendlyException("请选择所属合作伙伴"); + } + + await EnsurePartnerExistsAsync(partnerId); + + entity.GroupName = name; + entity.PartnerId = partnerId; + entity.State = input.State; + entity.LastModificationTime = Clock.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + return await GetAsync(groupId); + } + + /// + [UnitOfWork] + public async Task DeleteAsync(string id) + { + var groupId = id?.Trim(); + if (string.IsNullOrWhiteSpace(groupId)) + { + throw new UserFriendlyException("组织Id不能为空"); + } + + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); + if (entity is null) + { + throw new UserFriendlyException("组织不存在"); + } + + entity.IsDeleted = true; + entity.LastModificationTime = Clock.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + } + + /// + public async Task ExportPdfAsync(GroupGetListInputVo input) + { + QuestPDF.Settings.License = LicenseType.Community; + + var exportBase = BuildGroupJoinedQuery(input); + var count = await exportBase.CountAsync(); + if (count > ExportPdfMaxRows) + { + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); + } + + var rows = await BuildGroupJoinedQuery(input) + .Select((g, p) => new GroupGetListOutputDto + { + Id = g.Id, + GroupName = g.GroupName, + PartnerId = g.PartnerId, + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), + State = g.State, + CreationTime = g.CreationTime + }) + .Take(ExportPdfMaxRows) + .ToListAsync(); + + var fileName = $"groups_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; + var document = Document.Create(container => + { + container.Page(page => + { + page.Margin(28); + page.DefaultTextStyle(x => x.FontSize(10)); + page.Header().Text("Groups").SemiBold().FontSize(18); + page.Content().PaddingTop(12).Table(table => + { + table.ColumnsDefinition(c => + { + c.RelativeColumn(2.2f); + c.RelativeColumn(2.4f); + c.RelativeColumn(1f); + c.RelativeColumn(1.6f); + }); + + static IContainer CellHeader(IContainer c) => + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold()); + + table.Cell().Element(CellHeader).Text("Group Name"); + table.Cell().Element(CellHeader).Text("Parent Partner"); + table.Cell().Element(CellHeader).Text("Status"); + table.Cell().Element(CellHeader).Text("Created"); + + foreach (var e in rows) + { + var status = e.State ? "active" : "inactive"; + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(e.GroupName ?? string.Empty); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "无" : e.PartnerName); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); + } + }); + }); + }); + + var stream = new MemoryStream(); + document.GeneratePdf(stream); + stream.Position = 0; + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; + } + + private ISugarQueryable BuildGroupJoinedQuery(GroupGetListInputVo input) + { + var keyword = input.Keyword?.Trim(); + var partnerId = input.PartnerId?.Trim(); + + var query = _dbContext.SqlSugarClient.Queryable() + .LeftJoin((g, p) => g.PartnerId == p.Id && !p.IsDeleted) + .Where((g, p) => !g.IsDeleted) + .WhereIF(input.State != null, (g, p) => g.State == input.State) + .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), + (g, p) => g.GroupName.Contains(keyword!) || + (p.PartnerName != null && p.PartnerName.Contains(keyword!))); + + if (!string.IsNullOrWhiteSpace(input.Sorting)) + { + var sorting = input.Sorting.Trim(); + if (sorting.Equals("GroupName desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending((g, p) => g.GroupName); + } + else if (sorting.Equals("GroupName asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy((g, p) => g.GroupName); + } + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending((g, p) => g.CreationTime); + } + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy((g, p) => g.CreationTime); + } + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending((g, p) => g.State); + } + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy((g, p) => g.State); + } + else if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending((g, p) => p.PartnerName); + } + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy((g, p) => p.PartnerName); + } + else + { + query = query.OrderByDescending((g, p) => g.CreationTime); + } + } + else + { + query = query.OrderByDescending((g, p) => g.CreationTime); + } + + return query; + } + + private async Task EnsurePartnerExistsAsync(string partnerId) + { + var ok = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.Id == partnerId); + if (!ok) + { + throw new UserFriendlyException("所选合作伙伴不存在或已删除"); + } + } + + private async Task ResolvePartnerNameAsync(string partnerId) + { + var p = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); + if (p is null || string.IsNullOrWhiteSpace(p.PartnerName)) + { + return "无"; + } + + return p.PartnerName.Trim(); + } + + private static GroupGetOutputDto MapDetail(FlGroupDbEntity x, string partnerName) => new() + { + Id = x.Id, + GroupName = x.GroupName, + PartnerId = x.PartnerId, + PartnerName = partnerName, + State = x.State, + CreationTime = x.CreationTime, + LastModificationTime = x.LastModificationTime + }; + + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, + List items) + { + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); + return new PagedResultWithPageDto + { + PageIndex = pageIndex, + PageSize = pageSize, + TotalCount = total, + TotalPages = totalPages, + Items = items + }; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs index f17c480..64fbea7 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs +++ b/美国版/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 return new LabelGetOutputDto { - Id = label.LabelCode, + Id = label.LabelCode ?? string.Empty, LabelName = label.LabelName, LocationId = label.LocationId ?? string.Empty, LocationName = location?.LocationName ?? location?.LocationCode ?? "无", diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs index 52cab93..b4818b4 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs +++ b/美国版/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 var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) || + (x.DisplayText != null && x.DisplayText.Contains(keyword!))) .WhereIF(input.State != null, x => x.State == input.State); + // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 if (!string.IsNullOrWhiteSpace(input.Sorting)) { - query = query.OrderBy(input.Sorting); + var sorting = input.Sorting.Trim(); + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.OrderNum); + } + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.OrderNum); + } + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.CreationTime); + } + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.CreationTime); + } + else + { + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime); + } } else { @@ -58,8 +80,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ Id = x.Id, CategoryCode = x.CategoryCode, CategoryName = x.CategoryName, + DisplayText = x.DisplayText, CategoryPhotoUrl = x.CategoryPhotoUrl, State = x.State, + ButtonAppearance = x.ButtonAppearance, + AvailabilityType = x.AvailabilityType, OrderNum = x.OrderNum, NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, LastEdited = x.LastModificationTime ?? x.CreationTime @@ -77,7 +102,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ throw new UserFriendlyException("标签分类不存在"); } - return MapToGetOutput(entity); + var dto = MapToGetOutput(entity); + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + { + var locationIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.CategoryId == entity.Id) + .Select(x => x.LocationId) + .ToListAsync(); + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); + } + + return dto; } public async Task CreateAsync(LabelCategoryCreateInputVo input) @@ -89,6 +124,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ throw new UserFriendlyException("分类编码和名称不能为空"); } + var displayText = input.DisplayText?.Trim(); + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + var locationIds = NormalizeLocationIds(input.LocationIds); + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); if (duplicated) @@ -96,17 +137,23 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ throw new UserFriendlyException("分类编码或名称已存在"); } + var now = DateTime.Now; + var currentUserId = CurrentUser?.Id?.ToString(); var entity = new FlLabelCategoryDbEntity { Id = _guidGenerator.Create().ToString(), CategoryCode = code, CategoryName = name, - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl), State = input.State, + ButtonAppearance = appearance, + AvailabilityType = availabilityType, OrderNum = input.OrderNum }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -126,6 +173,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ throw new UserFriendlyException("分类编码和名称不能为空"); } + var displayText = input.DisplayText?.Trim(); + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + var locationIds = NormalizeLocationIds(input.LocationIds); + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); if (duplicated) @@ -135,13 +188,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ entity.CategoryCode = code; entity.CategoryName = name; - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText; + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl); entity.State = input.State; + entity.ButtonAppearance = appearance; + entity.AvailabilityType = availabilityType; entity.OrderNum = input.OrderNum; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); } @@ -174,12 +231,70 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ Id = x.Id, CategoryCode = x.CategoryCode, CategoryName = x.CategoryName, + DisplayText = x.DisplayText, CategoryPhotoUrl = x.CategoryPhotoUrl, State = x.State, + ButtonAppearance = x.ButtonAppearance, + AvailabilityType = x.AvailabilityType, OrderNum = x.OrderNum }; } + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List locationIds) + { + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") + { + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); + } + + if (availabilityType == "SPECIFIED" && locationIds.Count == 0) + { + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); + } + } + + private static List NormalizeLocationIds(List? locationIds) + { + return locationIds? + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct() + .ToList() ?? new(); + } + + private async Task SaveCategoryLocationsAsync( + string categoryId, + string availabilityType, + List locationIds, + string? currentUserId, + DateTime now) + { + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.CategoryId == categoryId) + .ExecuteCommandAsync(); + + if (availabilityType != "SPECIFIED") + { + return; + } + + if (locationIds.Count == 0) + { + return; + } + + var rows = locationIds.Select(locId => new FlLabelCategoryLocationDbEntity + { + Id = _guidGenerator.Create().ToString(), + CategoryId = categoryId, + LocationId = locId, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + } + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationSupportAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationSupportAppService.cs new file mode 100644 index 0000000..0421c09 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationSupportAppService.cs @@ -0,0 +1,161 @@ +using FoodLabeling.Application.Contracts; +using FoodLabeling.Application.Contracts.Dtos.LocationSupport; +using FoodLabeling.Application.Contracts.IServices; +using FoodLabeling.Application.Services.DbModels; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Guids; +using Volo.Abp.Uow; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// 全局 Support 联系方式(全门店共用;Web 可增改查,App JWT 仅可读) +/// +[Authorize] +public class LocationSupportAppService : ApplicationService, ILocationSupportAppService +{ + private readonly ISqlSugarDbContext _dbContext; + private readonly IGuidGenerator _guidGenerator; + + public LocationSupportAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) + { + _dbContext = dbContext; + _guidGenerator = guidGenerator; + } + + /// + public async Task GetSupportAsync() + { + var rows = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .ToListAsync(); + var entity = rows.FirstOrDefault(); + return MapOutput(entity); + } + + /// + [UnitOfWork] + public async Task CreateAsync(LocationSupportCreateInputVo input) + { + EnsureNotUsAppClient(); + + if (input is null) + { + throw new UserFriendlyException("Request body is required."); + } + + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required."); + var email = NormalizeRequired(input.SupportEmail, "Support email is required."); + EnsureEmailFormat(email); + + var existed = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted); + if (existed) + { + throw new UserFriendlyException( + "Global support contact already exists. Use update instead."); + } + + var now = Clock.Now; + var entity = new FlLocationSupportDbEntity + { + Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + CreationTime = now, + CreatorId = CurrentUser?.Id?.ToString(), + LastModificationTime = now, + LastModifierId = CurrentUser?.Id?.ToString(), + SupportPhone = phone, + SupportEmail = email + }; + + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + return MapOutput(entity)!; + } + + /// + [UnitOfWork] + public async Task UpdateAsync(string id, LocationSupportUpdateInputVo input) + { + EnsureNotUsAppClient(); + + var supportId = id?.Trim(); + if (string.IsNullOrWhiteSpace(supportId)) + { + throw new UserFriendlyException("Support record id is required."); + } + + if (input is null) + { + throw new UserFriendlyException("Request body is required."); + } + + var rows = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && x.Id == supportId) + .ToListAsync(); + var entity = rows.FirstOrDefault(); + if (entity is null) + { + throw new UserFriendlyException("Support record not found."); + } + + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required."); + var email = NormalizeRequired(input.SupportEmail, "Support email is required."); + EnsureEmailFormat(email); + + entity.SupportPhone = phone; + entity.SupportEmail = email; + entity.LastModificationTime = Clock.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + return MapOutput(entity)!; + } + + private void EnsureNotUsAppClient() + { + if (CurrentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value == UsAppJwtClaims.ClientKindUsApp) + { + throw new UserFriendlyException( + "The mobile app can only view support contacts. Please use the web console to edit."); + } + } + + private static string NormalizeRequired(string? value, string message) + { + var normalized = value?.Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw new UserFriendlyException(message); + } + + return normalized; + } + + private static void EnsureEmailFormat(string email) + { + if (!email.Contains("@", StringComparison.Ordinal) || email.StartsWith("@", StringComparison.Ordinal) || + email.EndsWith("@", StringComparison.Ordinal)) + { + throw new UserFriendlyException("Support email format is invalid."); + } + } + + private static LocationSupportGetOutputDto? MapOutput(FlLocationSupportDbEntity? entity) + { + if (entity is null) + { + return null; + } + + return new LocationSupportGetOutputDto + { + Id = entity.Id, + SupportPhone = entity.SupportPhone, + SupportEmail = entity.SupportEmail + }; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs new file mode 100644 index 0000000..321e705 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs @@ -0,0 +1,326 @@ +using FoodLabeling.Application.Helpers; +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Partner; +using FoodLabeling.Application.Contracts.IServices; +using FoodLabeling.Application.Services.DbModels; +using Microsoft.AspNetCore.Mvc; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Guids; +using Volo.Abp.Uow; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// 合作伙伴管理(fl_partner) +/// +public class PartnerAppService : ApplicationService, IPartnerAppService +{ + private const int ExportPdfMaxRows = 5000; + + private readonly ISqlSugarDbContext _dbContext; + private readonly IGuidGenerator _guidGenerator; + + public PartnerAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) + { + _dbContext = dbContext; + _guidGenerator = guidGenerator; + } + + /// + public async Task> GetListAsync(PartnerGetListInputVo input) + { + RefAsync total = 0; + var query = BuildPartnerListQuery(input); + + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + var items = entities.Select(MapListItem).ToList(); + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); + } + + /// + public async Task GetAsync(string id) + { + var partnerId = id?.Trim(); + if (string.IsNullOrWhiteSpace(partnerId)) + { + throw new UserFriendlyException("合作伙伴Id不能为空"); + } + + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); + if (entity is null) + { + throw new UserFriendlyException("合作伙伴不存在"); + } + + return MapDetail(entity); + } + + /// + [UnitOfWork] + public async Task CreateAsync(PartnerCreateInputVo input) + { + var name = input.PartnerName?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new UserFriendlyException("合作伙伴名称不能为空"); + } + + var email = input.ContactEmail?.Trim(); + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) + { + throw new UserFriendlyException("联系邮箱格式不正确"); + } + + var now = Clock.Now; + var entity = new FlPartnerDbEntity + { + Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + PartnerName = name, + ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email, + PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(), + State = input.State, + CreationTime = now, + CreatorId = CurrentUser?.Id?.ToString(), + LastModificationTime = now, + LastModifierId = CurrentUser?.Id?.ToString() + }; + + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + return await GetAsync(entity.Id); + } + + /// + [UnitOfWork] + public async Task UpdateAsync(string id, PartnerUpdateInputVo input) + { + var partnerId = id?.Trim(); + if (string.IsNullOrWhiteSpace(partnerId)) + { + throw new UserFriendlyException("合作伙伴Id不能为空"); + } + + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); + if (entity is null) + { + throw new UserFriendlyException("合作伙伴不存在"); + } + + var name = input.PartnerName?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new UserFriendlyException("合作伙伴名称不能为空"); + } + + var email = input.ContactEmail?.Trim(); + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) + { + throw new UserFriendlyException("联系邮箱格式不正确"); + } + + entity.PartnerName = name; + entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email; + entity.PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(); + entity.State = input.State; + entity.LastModificationTime = Clock.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + return await GetAsync(partnerId); + } + + /// + [UnitOfWork] + public async Task DeleteAsync(string id) + { + var partnerId = id?.Trim(); + if (string.IsNullOrWhiteSpace(partnerId)) + { + throw new UserFriendlyException("合作伙伴Id不能为空"); + } + + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); + if (entity is null) + { + throw new UserFriendlyException("合作伙伴不存在"); + } + + entity.IsDeleted = true; + entity.LastModificationTime = Clock.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + } + + /// + public async Task ExportPdfAsync(PartnerGetListInputVo input) + { + QuestPDF.Settings.License = LicenseType.Community; + + var count = await BuildPartnerListQuery(input).CountAsync(); + var query = BuildPartnerListQuery(input); + if (count > ExportPdfMaxRows) + { + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); + } + + var rows = await query.Take(ExportPdfMaxRows).ToListAsync(); + var fileName = $"partners_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; + + var document = Document.Create(container => + { + container.Page(page => + { + page.Margin(28); + page.DefaultTextStyle(x => x.FontSize(10)); + page.Header().Text("Partners").SemiBold().FontSize(18); + page.Content().PaddingTop(12).Table(table => + { + table.ColumnsDefinition(c => + { + c.RelativeColumn(2.2f); + c.RelativeColumn(2.4f); + c.RelativeColumn(1.8f); + c.RelativeColumn(1f); + c.RelativeColumn(1.6f); + }); + + static IContainer CellHeader(IContainer c) => + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold()); + + table.Cell().Element(CellHeader).Text("Partner"); + table.Cell().Element(CellHeader).Text("Contact"); + table.Cell().Element(CellHeader).Text("Phone"); + table.Cell().Element(CellHeader).Text("Status"); + table.Cell().Element(CellHeader).Text("Created"); + + foreach (var e in rows) + { + var status = e.State ? "active" : "inactive"; + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(e.PartnerName ?? string.Empty); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "无" : e.ContactEmail!); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "无" : e.PhoneNumber!); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); + } + }); + }); + }); + + var stream = new MemoryStream(); + document.GeneratePdf(stream); + stream.Position = 0; + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; + } + + private ISugarQueryable BuildPartnerListQuery(PartnerGetListInputVo input) + { + var keyword = input.Keyword?.Trim(); + var query = _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .WhereIF(input.State != null, x => x.State == input.State) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), + x => x.PartnerName.Contains(keyword!) || + (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) || + (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!))); + + if (!string.IsNullOrWhiteSpace(input.Sorting)) + { + var sorting = input.Sorting.Trim(); + if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.PartnerName); + } + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.PartnerName); + } + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.CreationTime); + } + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.CreationTime); + } + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.State); + } + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.State); + } + else + { + query = query.OrderByDescending(x => x.CreationTime); + } + } + else + { + query = query.OrderByDescending(x => x.CreationTime); + } + + return query; + } + + private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) => new() + { + Id = x.Id, + PartnerName = x.PartnerName, + ContactEmail = x.ContactEmail, + PhoneNumber = x.PhoneNumber, + State = x.State, + CreationTime = x.CreationTime + }; + + private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) => new() + { + Id = x.Id, + PartnerName = x.PartnerName, + ContactEmail = x.ContactEmail, + PhoneNumber = x.PhoneNumber, + State = x.State, + CreationTime = x.CreationTime, + LastModificationTime = x.LastModificationTime + }; + + private static bool IsPlausibleEmail(string email) + { + if (email.Length > 256) + { + return false; + } + + var at = email.IndexOf('@'); + return at > 0 && at < email.Length - 1 && email.IndexOf('@', at + 1) < 0; + } + + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, + List items) + { + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); + return new PagedResultWithPageDto + { + PageIndex = pageIndex, + PageSize = pageSize, + TotalCount = total, + TotalPages = totalPages, + Items = items + }; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs index 0bd17df..6b04d91 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs +++ b/美国版/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; using FoodLabeling.Application.Contracts.Dtos.Product; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; @@ -188,6 +189,13 @@ public class ProductAppService : ApplicationService, IProductAppService }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + + if (input.LocationIds is not null) + { + var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); + await ReplaceProductLocationLinksAsync(entity.Id, locIds); + } + return await GetAsync(entity.Id); } @@ -228,6 +236,13 @@ public class ProductAppService : ApplicationService, IProductAppService entity.State = input.State; await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + + if (input.LocationIds is not null) + { + var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); + await ReplaceProductLocationLinksAsync(productId, locIds); + } + return await GetAsync(productId); } @@ -251,6 +266,66 @@ public class ProductAppService : ApplicationService, IProductAppService await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); } + /// + /// 去重、校验门店 Id 格式与存在性。 + /// + private async Task> NormalizeAndValidateLocationIdsAsync(IEnumerable rawIds) + { + var distinct = rawIds + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (distinct.Count == 0) + { + return new List(); + } + + foreach (var id in distinct) + { + if (!Guid.TryParse(id, out _)) + { + throw new UserFriendlyException("门店Id格式不正确"); + } + } + + var guidList = distinct.Select(Guid.Parse).ToList(); + var existCount = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) + .CountAsync(); + if (existCount != distinct.Count) + { + throw new UserFriendlyException("门店不存在"); + } + + return distinct; + } + + /// + /// 按产品维度替换 fl_location_product:先删本产品全部关联,再按列表插入(每门店一行)。 + /// + private async Task ReplaceProductLocationLinksAsync(string productId, List locationIds) + { + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.ProductId == productId) + .ExecuteCommandAsync(); + + if (locationIds.Count == 0) + { + return; + } + + var rows = locationIds.Select(lid => new FlLocationProductDbEntity + { + Id = _guidGenerator.Create().ToString(), + LocationId = lid, + ProductId = productId + }).ToList(); + + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + } + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs index 3a6fdf6..a66c2f2 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs @@ -1,3 +1,4 @@ +using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.ProductCategory; using FoodLabeling.Application.Contracts.IServices; @@ -35,7 +36,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) || + (x.DisplayText != null && x.DisplayText.Contains(keyword!))) .WhereIF(input.State != null, x => x.State == input.State); // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column @@ -77,8 +79,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp Id = x.Id, CategoryCode = x.CategoryCode, CategoryName = x.CategoryName, + DisplayText = x.DisplayText, CategoryPhotoUrl = x.CategoryPhotoUrl, + ButtonAppearance = x.ButtonAppearance, State = x.State, + AvailabilityType = x.AvailabilityType, OrderNum = x.OrderNum, LastEdited = x.LastModificationTime ?? x.CreationTime }).ToList(); @@ -98,7 +103,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp throw new UserFriendlyException("类别不存在"); } - return MapToGetOutput(entity); + var dto = MapToGetOutput(entity); + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + { + var locationIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.CategoryId == entity.Id) + .Select(x => x.LocationId) + .ToListAsync(); + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); + } + + return dto; } /// @@ -113,6 +128,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp throw new UserFriendlyException("类别编码和名称不能为空"); } + var displayText = input.DisplayText?.Trim(); + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + var locationIds = NormalizeLocationIds(input.LocationIds); + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); if (duplicated) @@ -133,12 +154,16 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp ConcurrencyStamp = _guidGenerator.Create().ToString("N"), CategoryCode = code, CategoryName = name, - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl), + ButtonAppearance = appearance, State = input.State, + AvailabilityType = availabilityType, OrderNum = input.OrderNum }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -161,6 +186,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp throw new UserFriendlyException("类别编码和名称不能为空"); } + var displayText = input.DisplayText?.Trim(); + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + var locationIds = NormalizeLocationIds(input.LocationIds); + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); if (duplicated) @@ -170,13 +201,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp entity.CategoryCode = code; entity.CategoryName = name; - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText; + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl); + entity.ButtonAppearance = appearance; entity.State = input.State; + entity.AvailabilityType = availabilityType; entity.OrderNum = input.OrderNum; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); } @@ -213,12 +248,70 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp Id = x.Id, CategoryCode = x.CategoryCode, CategoryName = x.CategoryName, + DisplayText = x.DisplayText, CategoryPhotoUrl = x.CategoryPhotoUrl, + ButtonAppearance = x.ButtonAppearance, State = x.State, + AvailabilityType = x.AvailabilityType, OrderNum = x.OrderNum }; } + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List locationIds) + { + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") + { + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); + } + + if (availabilityType == "SPECIFIED" && locationIds.Count == 0) + { + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); + } + } + + private static List NormalizeLocationIds(List? locationIds) + { + return locationIds? + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct() + .ToList() ?? new(); + } + + private async Task SaveCategoryLocationsAsync( + string categoryId, + string availabilityType, + List locationIds, + string? currentUserId, + DateTime now) + { + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.CategoryId == categoryId) + .ExecuteCommandAsync(); + + if (availabilityType != "SPECIFIED") + { + return; + } + + if (locationIds.Count == 0) + { + return; + } + + var rows = locationIds.Select(locId => new FlProductCategoryLocationDbEntity + { + Id = _guidGenerator.Create().ToString(), + CategoryId = categoryId, + LocationId = locId, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + } + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacMenuAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacMenuAppService.cs index e23abcd..66868ba 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacMenuAppService.cs +++ b/美国版/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 Id = x.Id, ParentId = x.ParentId, MenuName = x.MenuName ?? string.Empty, + RouterName = x.RouterName, + Router = x.Router, PermissionCode = x.PermissionCode, MenuType = x.MenuType, MenuSource = x.MenuSource, @@ -62,6 +64,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService Id = entity.Id, ParentId = entity.ParentId, MenuName = entity.MenuName ?? string.Empty, + RouterName = entity.RouterName, + Router = entity.Router, PermissionCode = entity.PermissionCode, MenuType = entity.MenuType, MenuSource = entity.MenuSource, diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs new file mode 100644 index 0000000..8640cec --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs @@ -0,0 +1,777 @@ +using System.Globalization; +using System.Text.Json; +using FoodLabeling.Application.Helpers; +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Reports; +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; +using FoodLabeling.Application.Contracts.IServices; +using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// Reports(Print Log / Label Report) +/// +[Authorize] +public class ReportsAppService : ApplicationService, IReportsAppService +{ + private const int ExportPdfMaxRows = 5000; + + private readonly ISqlSugarDbContext _dbContext; + private readonly IUsAppLabelingAppService _usAppLabelingAppService; + + public ReportsAppService(ISqlSugarDbContext dbContext, IUsAppLabelingAppService usAppLabelingAppService) + { + _dbContext = dbContext; + _usAppLabelingAppService = usAppLabelingAppService; + } + + /// + public async Task> GetPrintLogListAsync( + ReportsPrintLogGetListInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + if (locationIds is not null && locationIds.Count == 0) + { + return EmptyPrintLogPage(input); + } + + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var currentUserIdStr = CurrentUser.Id.Value.ToString(); + var keyword = input.Keyword?.Trim(); + + RefAsync total = 0; + + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) + .Where((t, l, p, lc, pc, loc, tpl) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl); + + if (!string.IsNullOrWhiteSpace(input.Sorting) && + input.Sorting.Trim().Equals("PrintedAt asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), + OrderByType.Asc); + } + else + { + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), + OrderByType.Desc); + } + + var pageRows = await query + .Select((t, l, p, lc, pc, loc, tpl) => new + { + t.Id, + LabelCode = l.LabelCode, + ProductName = p.ProductName, + LabelCategoryName = lc.CategoryName, + ProductCategoryName = pc.CategoryName, + tpl.Width, + tpl.Height, + tpl.Unit, + tpl.TemplateName, + t.PrintInputJson, + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), + t.CreatedBy, + t.LocationId, + LocName = loc.LocationName, + LocCode = loc.LocationCode + }) + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + + var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x!).Distinct().ToList()); + + var items = pageRows.Select(x => + { + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) + ? x.ProductCategoryName!.Trim() + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); + var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); + var locText = FormatLocationText(x.LocName, x.LocCode); + var printedAt = x.PrintedAt ?? DateTime.MinValue; + return new ReportsPrintLogListItemDto + { + TaskId = x.Id, + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), + CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, + TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, + PrintedAt = printedAt, + PrintedByName = ResolveUserName(userMap, x.CreatedBy), + LocationText = locText, + LocationId = x.LocationId?.Trim(), + ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) + }; + }).ToList(); + + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); + } + + /// + public async Task ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input) + { + QuestPDF.Settings.License = LicenseType.Community; + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + if (locationIds is not null && locationIds.Count == 0) + { + return BuildEmptyPdf("print-log-empty.pdf"); + } + + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var currentUserIdStr = CurrentUser.Id.Value.ToString(); + var keyword = input.Keyword?.Trim(); + + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) + .Where((t, l, p, lc, pc, loc, tpl) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) + .OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc); + + var count = await query.CountAsync(); + if (count > ExportPdfMaxRows) + { + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); + } + + var rows = await query.Take(ExportPdfMaxRows) + .Select((t, l, p, lc, pc, loc, tpl) => new + { + t.Id, + LabelCode = l.LabelCode, + ProductName = p.ProductName, + LabelCategoryName = lc.CategoryName, + ProductCategoryName = pc.CategoryName, + tpl.Width, + tpl.Height, + tpl.Unit, + tpl.TemplateName, + t.PrintInputJson, + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), + t.CreatedBy, + LocName = loc.LocationName, + LocCode = loc.LocationCode + }) + .ToListAsync(); + + var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x!).Distinct().ToList()); + + var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; + var document = Document.Create(container => + { + container.Page(page => + { + page.Margin(22); + page.DefaultTextStyle(x => x.FontSize(8.5f)); + page.Header().Text("Print Log").SemiBold().FontSize(16); + page.Content().PaddingTop(10).Table(table => + { + table.ColumnsDefinition(c => + { + c.RelativeColumn(1.1f); + c.RelativeColumn(1.2f); + c.RelativeColumn(0.9f); + c.RelativeColumn(1.1f); + c.RelativeColumn(1f); + c.RelativeColumn(0.9f); + c.RelativeColumn(0.9f); + c.RelativeColumn(0.8f); + }); + static IContainer H(IContainer x) => + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); + table.Cell().Element(H).Text("Label ID"); + table.Cell().Element(H).Text("Product"); + table.Cell().Element(H).Text("Category"); + table.Cell().Element(H).Text("Template"); + table.Cell().Element(H).Text("Printed At"); + table.Cell().Element(H).Text("Printed By"); + table.Cell().Element(H).Text("Location"); + table.Cell().Element(H).Text("Expiry"); + + foreach (var x in rows) + { + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) + ? x.ProductCategoryName!.Trim() + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim()); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName)); + var printedAt = x.PrintedAt ?? DateTime.MinValue; + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(printedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(ResolveUserName(userMap, x.CreatedBy)); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(FormatLocationText(x.LocName, x.LocCode)); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(TryExtractExpiryText(x.PrintInputJson)); + } + }); + }); + }); + + var ms = new MemoryStream(); + document.GeneratePdf(ms); + ms.Position = 0; + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; + } + + /// + public Task ReprintPrintLogAsync(UsAppLabelReprintInputVo input) => + _usAppLabelingAppService.ReprintAsync(input); + + /// + public async Task GetLabelReportAsync(ReportsLabelReportQueryInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + if (locationIds is not null && locationIds.Count == 0) + { + return new ReportsLabelReportOutputDto(); + } + + var (curStart, curEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); + var span = curEndExcl - curStart; + if (span.TotalDays < 1) + { + span = TimeSpan.FromDays(1); + } + + var prevEndExcl = curStart; + var prevStart = curStart - span; + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var currentUserIdStr = CurrentUser.Id.Value.ToString(); + var keyword = input.Keyword?.Trim(); + + var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .Where((t, l, p, lc, pc, loc) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) + .CountAsync(); + + var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .Where((t, l, p, lc, pc, loc) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) + .CountAsync(); + + var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays)); + var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays)); + var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); + var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); + + var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .Where((t, l, p, lc, pc, loc) => + l.LabelCategoryId != null && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) + .GroupBy((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName }) + .Select((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) + .ToListAsync(); + + var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); + + var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .Where((t, l, p, lc, pc, loc) => + !string.IsNullOrEmpty(p.Id) && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) + .GroupBy((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, Cat = pc.CategoryName }) + .Select((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, CategoryName = pc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) + .ToListAsync(); + + var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); + var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList(); + + var trendEndDay = curEndExcl.Date.AddDays(-1); + var trendStartDay = trendEndDay.AddDays(-6); + if (trendStartDay < curStart.Date) + { + trendStartDay = curStart.Date; + } + + var trendEndExcl = trendEndDay.AddDays(1); + + var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + .Where((t, l, p, lc, pc, loc) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) + .Select((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime)) + .ToListAsync(); + + var trendDict = trendRaw + .Where(x => x.HasValue) + .GroupBy(x => x!.Value.Date) + .ToDictionary(g => g.Key, g => g.Count()); + + var trend = new List(); + for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1)) + { + trend.Add(new ReportsDailyCountDto + { + Date = d.ToString("yyyy-MM-dd"), + Count = trendDict.TryGetValue(d, out var c) ? c : 0 + }); + } + + var byCategory = categoryRows + .OrderByDescending(x => x.Cnt) + .Select(x => new ReportsCategoryCountDto + { + CategoryId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName.Trim(), + Count = x.Cnt + }) + .ToList(); + + var mostUsed = topList.Select(x => + { + var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2); + return new ReportsTopProductRowDto + { + ProductId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? null : x.ProductName.Trim(), + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName!.Trim(), + TotalPrinted = x.Cnt, + UsagePercent = pct + }; + }).ToList(); + + return new ReportsLabelReportOutputDto + { + Summary = new ReportsLabelReportSummaryDto + { + TotalLabelsPrinted = totalCur, + TotalLabelsPrintedPrevPeriod = totalPrev, + TotalLabelsPrintedChangeRate = CalcChangeRate(totalCur, totalPrev), + MostPrintedCategoryName = string.IsNullOrWhiteSpace(topCat?.CategoryName) ? null : topCat.CategoryName.Trim(), + MostPrintedCategoryCount = topCat?.Cnt ?? 0, + TopProductName = string.IsNullOrWhiteSpace(topProd?.ProductName) ? null : topProd.ProductName.Trim(), + TopProductCount = topProd?.Cnt ?? 0, + AvgDailyPrints = avgDaily, + AvgDailyPrintsPrevPeriod = avgDailyPrev, + AvgDailyPrintsChangeRate = CalcChangeRate(avgDaily, avgDailyPrev) + }, + LabelsByCategory = byCategory, + PrintVolumeTrend = trend, + MostUsedProducts = mostUsed + }; + } + + /// + public async Task ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input) + { + QuestPDF.Settings.License = LicenseType.Community; + var data = await GetLabelReportAsync(input); + var fileName = $"label-report_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; + var document = Document.Create(container => + { + container.Page(page => + { + page.Margin(24); + page.DefaultTextStyle(x => x.FontSize(9)); + page.Header().Text("Label Report").SemiBold().FontSize(16); + page.Content().Column(col => + { + col.Spacing(10); + col.Item().Text( + $"Total printed: {data.Summary.TotalLabelsPrinted} (prev: {data.Summary.TotalLabelsPrintedPrevPeriod}, Δ%: {data.Summary.TotalLabelsPrintedChangeRate:0.##}%)"); + col.Item().Text( + $"Top category: {data.Summary.MostPrintedCategoryName} ({data.Summary.MostPrintedCategoryCount})"); + col.Item().Text($"Top product: {data.Summary.TopProductName} ({data.Summary.TopProductCount})"); + col.Item().Text( + $"Avg daily: {data.Summary.AvgDailyPrints:0.##} (prev: {data.Summary.AvgDailyPrintsPrevPeriod:0.##}, Δ%: {data.Summary.AvgDailyPrintsChangeRate:0.##}%)"); + col.Item().Text("By category:").SemiBold(); + col.Item().Table(t => + { + t.ColumnsDefinition(c => { c.RelativeColumn(2); c.RelativeColumn(1); }); + t.Cell().Element(HeaderCell).Text("Category"); + t.Cell().Element(HeaderCell).Text("Count"); + foreach (var r in data.LabelsByCategory) + { + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); + } + }); + col.Item().Text("Daily trend:").SemiBold(); + col.Item().Table(t => + { + t.ColumnsDefinition(c => { c.RelativeColumn(1.2f); c.RelativeColumn(1); }); + t.Cell().Element(HeaderCell).Text("Date"); + t.Cell().Element(HeaderCell).Text("Count"); + foreach (var r in data.PrintVolumeTrend) + { + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Date); + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); + } + }); + col.Item().Text("Most used products:").SemiBold(); + col.Item().Table(t => + { + t.ColumnsDefinition(c => + { + c.RelativeColumn(1.5f); + c.RelativeColumn(1f); + c.RelativeColumn(0.8f); + c.RelativeColumn(0.7f); + }); + t.Cell().Element(HeaderCell).Text("Product"); + t.Cell().Element(HeaderCell).Text("Category"); + t.Cell().Element(HeaderCell).Text("Total"); + t.Cell().Element(HeaderCell).Text("%"); + foreach (var r in data.MostUsedProducts) + { + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.ProductName); + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.TotalPrinted.ToString()); + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.UsagePercent.ToString("0.##")); + } + }); + }); + }); + }); + + static IContainer HeaderCell(IContainer x) => + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); + + var ms = new MemoryStream(); + document.GeneratePdf(ms); + ms.Position = 0; + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; + } + + private ISugarQueryable + BuildReportTaskCore( + List? locationIds, + bool isAdmin, + string currentUserIdStr, + string? keyword) + { + return _dbContext.SqlSugarClient.Queryable() + .LeftJoin((t, l) => t.LabelId == l.Id) + .LeftJoin((t, l, p) => t.ProductId == p.Id) + .LeftJoin((t, l, p, lc) => l.LabelCategoryId == lc.Id) + .LeftJoin((t, l, p, lc, pc) => p.CategoryId == pc.Id) + .LeftJoin((t, l, p, lc, pc, loc) => + t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) + .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) + .WhereIF(!isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) + .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), + (t, l, p, lc, pc, loc) => + (p.ProductName != null && p.ProductName.Contains(keyword!)) || + (lc.CategoryName != null && lc.CategoryName.Contains(keyword!)) || + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!))); + } + + private static decimal CalcChangeRate(decimal current, decimal previous) + { + if (previous == 0) + { + return current > 0 ? 100m : 0m; + } + + return Math.Round((current - previous) * 100m / previous, 2); + } + + private static decimal CalcChangeRate(int current, int previous) => + CalcChangeRate((decimal)current, (decimal)previous); + + private async Task?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, + string? locationId) + { + var locId = locationId?.Trim(); + if (!string.IsNullOrWhiteSpace(locId)) + { + return new List { locId }; + } + + var gid = groupId?.Trim(); + var pid = partnerId?.Trim(); + + if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) + { + return null; + } + + var q = _dbContext.SqlSugarClient.Queryable().Where(x => !x.IsDeleted); + + if (!string.IsNullOrWhiteSpace(gid)) + { + var g = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == gid); + if (g is null) + { + return new List(); + } + + var gName = g.GroupName?.Trim() ?? string.Empty; + var partner = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); + var pName = partner?.PartnerName?.Trim() ?? string.Empty; + q = q.Where(x => x.GroupName == gName && x.Partner == pName); + } + else if (!string.IsNullOrWhiteSpace(pid)) + { + var partner = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == pid); + if (partner is null) + { + return new List(); + } + + var pName = partner.PartnerName?.Trim() ?? string.Empty; + q = q.Where(x => x.Partner == pName); + } + + var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); + return ids; + } + + private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, + DateTime? endDate) + { + var endDay = (endDate ?? DateTime.Today).Date; + var endExcl = endDay.AddDays(1); + var start = (startDate ?? endDay.AddDays(-29)).Date; + if (start >= endExcl) + { + start = endExcl.AddDays(-1); + } + + return (start, endExcl); + } + + private static PagedResultWithPageDto EmptyPrintLogPage( + ReportsPrintLogGetListInputVo input) + { + var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); + return new PagedResultWithPageDto + { + PageIndex = pageIndex, + PageSize = pageSize, + TotalCount = 0, + TotalPages = 0, + Items = new List() + }; + } + + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, + List items) + { + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); + return new PagedResultWithPageDto + { + PageIndex = pageIndex, + PageSize = pageSize, + TotalCount = total, + TotalPages = totalPages, + Items = items + }; + } + + private async Task> LoadUserNameMapAsync(List userIdStrings) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (userIdStrings.Count == 0) + { + return map; + } + + var guids = userIdStrings + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .Distinct() + .ToList(); + if (guids.Count == 0) + { + return map; + } + + var users = await _dbContext.SqlSugarClient.Queryable() + .Where(u => !u.IsDeleted && guids.Contains(u.Id)) + .Select(u => new { u.Id, u.Name, u.UserName }) + .ToListAsync(); + + foreach (var u in users) + { + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); + map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; + } + + return map; + } + + private static string ResolveUserName(Dictionary map, string? createdBy) + { + if (string.IsNullOrWhiteSpace(createdBy)) + { + return "无"; + } + + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; + } + + private static string FormatLocationText(string? locName, string? locCode) + { + var n = locName?.Trim(); + var c = locCode?.Trim(); + if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(c)) + { + return "无"; + } + + if (string.IsNullOrWhiteSpace(c)) + { + return n ?? "无"; + } + + if (string.IsNullOrWhiteSpace(n)) + { + return $"({c})"; + } + + return $"{n} ({c})"; + } + + private static string FormatTemplateDisplay(decimal w, decimal h, string? unit, string? templateName) + { + var size = FormatLabelSizeWithUnit(w, h, unit ?? "inch"); + var tn = templateName?.Trim(); + if (string.IsNullOrWhiteSpace(tn)) + { + return size ?? "无"; + } + + return string.IsNullOrWhiteSpace(size) ? tn : $"{size} {tn}"; + } + + private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit) + { + var u = (unit ?? "inch").Trim().ToLowerInvariant(); + var ws = w.ToString(CultureInfo.InvariantCulture); + var hs = h.ToString(CultureInfo.InvariantCulture); + var normalizedUnit = u is "in" ? "inch" : u; + return $"{ws}x{hs}{normalizedUnit}"; + } + + private static string TryExtractExpiryText(string? printInputJson) + { + if (string.IsNullOrWhiteSpace(printInputJson)) + { + return "无"; + } + + try + { + using var doc = JsonDocument.Parse(printInputJson); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + return "无"; + } + + foreach (var prop in doc.RootElement.EnumerateObject()) + { + var key = prop.Name.Trim(); + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var v = prop.Value; + if (v.ValueKind == JsonValueKind.String) + { + var s = v.GetString(); + return string.IsNullOrWhiteSpace(s) ? "无" : s.Trim(); + } + + if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n)) + { + return n.ToString(CultureInfo.InvariantCulture); + } + + return v.ToString(); + } + } + catch + { + return "无"; + } + + return "无"; + } + + private static IActionResult BuildEmptyPdf(string fileName) + { + QuestPDF.Settings.License = LicenseType.Community; + var document = Document.Create(c => + { + c.Page(p => + { + p.Margin(30); + p.Content().Text("No data for current filters."); + }); + }); + var ms = new MemoryStream(); + document.GeneratePdf(ms); + ms.Position = 0; + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs index 93cd84c..1a8fd09 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; +using FoodLabeling.Application.Contracts; using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; @@ -20,6 +22,7 @@ using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.EventBus.Local; using Volo.Abp.Security.Claims; +using Volo.Abp.Uow; using Volo.Abp.Users; using Yi.Framework.Core.Helper; using Yi.Framework.Rbac.Domain.Entities; @@ -135,6 +138,267 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService return await LoadBoundLocationsAsync(CurrentUser.Id.Value); } + /// + /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户) + /// + /// + /// 仅当当前登录用户在 userlocation 中绑定该 locationId 时可查;否则返回业务异常。 + /// + /// 店长:在同店绑定用户中,取 Role.RoleCodeRole.RoleName(忽略大小写)包含 manager 的第一条; + /// 若无匹配则店长姓名与电话均为「无」。 + /// + /// OperatingHours:当前 location 表无营业时间字段,固定返回「无」。 + /// + /// 门店主键(Guid 字符串) + /// 与原型一致的展示字段 + /// 成功 + /// 未登录、门店标识无效、未绑定或门店不存在 + /// 服务器错误 + [Authorize] + public virtual async Task GetLocationDetailAsync(string locationId) + { + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var lid = (locationId ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(lid) || !Guid.TryParse(lid, out var locationGuid)) + { + throw new UserFriendlyException("无效的门店标识"); + } + + var userIdStr = CurrentUser.Id.Value.ToString(); + var bound = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid); + if (!bound) + { + throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); + } + + var locRows = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && x.Id == locationGuid) + .ToListAsync(); + var loc = locRows.FirstOrDefault(); + if (loc is null) + { + throw new UserFriendlyException("门店不存在或已删除"); + } + + var (mgrName, mgrPhone) = await TryResolveStoreManagerAsync(lid); + + return new UsAppLocationDetailOutputDto + { + LocationId = loc.Id.ToString(), + LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(), + FullAddress = BuildFullAddress(loc), + StorePhone = FormatStorePhoneDisplay(loc.Phone), + OperatingHours = "无", + ManagerName = mgrName, + ManagerPhone = mgrPhone + }; + } + + /// + [Authorize] + public virtual async Task GetMyProfileAsync() + { + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var userId = CurrentUser.Id.Value; + var user = await _userRepository.GetByIdAsync(userId); + if (user is null || user.IsDeleted || !user.State) + { + throw new UserFriendlyException("用户不存在或已停用"); + } + + // 避免 SqlSugar 在该环境下对 Role 关联表达式解析异常(Select 不支持),这里改用显式 SQL 查询角色。 + var roleRows = await _dbContext.SqlSugarClient.Ado.SqlQueryAsync( + @"SELECT r.RoleName, r.RoleCode + FROM UserRole ur + INNER JOIN Role r ON ur.RoleId = r.Id + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 + ORDER BY r.OrderNum ASC", + new { UserId = userId }); + + var roleNames = roleRows.Select(x => x.RoleName?.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); + var roleDisplay = roleNames.Count == 0 ? "无" : string.Join(", ", roleNames); + var primaryCode = roleRows.FirstOrDefault()?.RoleCode?.Trim(); + + var fullName = !string.IsNullOrWhiteSpace(user.Name?.Trim()) + ? user.Name.Trim() + : (!string.IsNullOrWhiteSpace(user.Nick?.Trim()) ? user.Nick.Trim() : user.UserName.Trim()); + + return new UsAppMyProfileOutputDto + { + FullName = fullName, + Email = string.IsNullOrWhiteSpace(user.Email) ? "无" : user.Email.Trim(), + Phone = FormatPhoneDisplay(user.Phone), + EmployeeId = string.IsNullOrWhiteSpace(user.UserName) ? "无" : user.UserName.Trim(), + RoleDisplay = roleDisplay, + PrimaryRoleCode = string.IsNullOrWhiteSpace(primaryCode) ? null : primaryCode + }; + } + + /// + [Authorize] + [UnitOfWork] + public virtual async Task ChangePasswordAsync(UsAppChangePasswordInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var current = input.CurrentPassword ?? string.Empty; + var newPwd = input.NewPassword ?? string.Empty; + var confirm = input.ConfirmNewPassword ?? string.Empty; + + if (string.IsNullOrWhiteSpace(current) || string.IsNullOrWhiteSpace(newPwd) || string.IsNullOrWhiteSpace(confirm)) + { + throw new UserFriendlyException("请填写当前密码、新密码与确认密码"); + } + + if (!string.Equals(newPwd, confirm, StringComparison.Ordinal)) + { + throw new UserFriendlyException("新密码与确认密码不一致"); + } + + if (string.Equals(current, newPwd, StringComparison.Ordinal)) + { + throw new UserFriendlyException("新密码不能与当前密码相同"); + } + + ValidateAppPasswordComplexity(newPwd); + + var userId = CurrentUser.Id.Value; + var user = await _userRepository.GetByIdAsync(userId); + if (user is null || user.IsDeleted || !user.State) + { + throw new UserFriendlyException("用户不存在或已停用"); + } + + if (!user.JudgePassword(current)) + { + throw new UserFriendlyException(UserConst.Login_Error); + } + + user.EncryPassword.Password = newPwd; + user.BuildPassword(); + await _userRepository.UpdateAsync(user); + } + + private static string FormatPhoneDisplay(long? phone) + { + if (!phone.HasValue) + { + return "无"; + } + + var digits = Math.Abs(phone.Value).ToString(CultureInfo.InvariantCulture); + if (digits.Length == 10) + { + return $"+1 ({digits[..3]}) {digits.Substring(3, 3)}-{digits.Substring(6, 4)}"; + } + + if (digits.Length == 11 && digits.StartsWith("1", StringComparison.Ordinal)) + { + return $"+1 ({digits[1..4]}) {digits.Substring(4, 3)}-{digits.Substring(7, 4)}"; + } + + return $"+{digits}"; + } + + private static string FormatStorePhoneDisplay(string? phone) + { + var t = phone?.Trim(); + return string.IsNullOrEmpty(t) ? "无" : t; + } + + private async Task<(string Name, string Phone)> TryResolveStoreManagerAsync(string locationIdTrimmed) + { + var userIdStrings = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && x.LocationId == locationIdTrimmed) + .Select(x => x.UserId) + .Distinct() + .ToListAsync(); + + var userGuids = userIdStrings + .Select(s => Guid.TryParse(s, out var g) ? (Guid?)g : null) + .Where(g => g.HasValue) + .Select(g => g!.Value) + .ToList(); + + if (userGuids.Count == 0) + { + return ("无", "无"); + } + + var rows = await _dbContext.SqlSugarClient.Ado.SqlQueryAsync( + @"SELECT u.Name, u.Nick, u.UserName, u.Phone + FROM User u + INNER JOIN UserRole ur ON u.Id = ur.UserId + INNER JOIN Role r ON ur.RoleId = r.Id + WHERE u.IsDeleted = 0 + AND u.State = 1 + AND r.IsDeleted = 0 + AND r.State = 1 + AND (LOWER(r.RoleCode) LIKE '%manager%' OR LOWER(r.RoleName) LIKE '%manager%') + AND u.Id IN (@UserIds) + ORDER BY u.Name ASC", + new { UserIds = userGuids }); + + var row = rows.FirstOrDefault(); + if (row is null) + { + return ("无", "无"); + } + + var displayName = !string.IsNullOrWhiteSpace(row.Name?.Trim()) + ? row.Name!.Trim() + : (!string.IsNullOrWhiteSpace(row.Nick?.Trim()) + ? row.Nick!.Trim() + : (row.UserName?.Trim() ?? "无")); + + return (displayName, FormatPhoneDisplay(row.Phone)); + } + + private static void ValidateAppPasswordComplexity(string password) + { + if (password.Length < 8) + { + throw new UserFriendlyException("新密码至少 8 位"); + } + + if (!password.Any(char.IsUpper)) + { + throw new UserFriendlyException("新密码需包含大写字母"); + } + + if (!password.Any(char.IsLower)) + { + throw new UserFriendlyException("新密码需包含小写字母"); + } + + if (!password.Any(char.IsDigit)) + { + throw new UserFriendlyException("新密码需包含至少一个数字"); + } + + if (!password.Any(c => !char.IsLetterOrDigit(c))) + { + throw new UserFriendlyException("新密码需包含至少一个特殊字符"); + } + } + private void ValidationImageCaptcha(string? uuid, string? code) { if (!_rbacOptions.EnableCaptcha) @@ -149,16 +413,21 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService } /// - /// 按邮箱查找未删除且启用的用户(邮箱比较忽略大小写) + /// 按邮箱或用户名(邮箱形字符串写在 UserName 时)查找未删除且启用的用户;比较忽略大小写,Email 命中优先。 /// private async Task FindActiveUserByEmailAsync(string email) { var normalized = email.Trim().ToLowerInvariant(); var users = await _userRepository._DbQueryable .Where(u => !u.IsDeleted && u.State == true) - .Where(u => u.Email != null && SqlFunc.ToLower(u.Email) == normalized) + .Where(u => + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) || + SqlFunc.ToLower(u.UserName) == normalized) .ToListAsync(); - return users.FirstOrDefault(); + return users.FirstOrDefault(u => + u.Email != null && + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase)) + ?? users.FirstOrDefault(); } private string CreateAppAccessToken(UserAggregateRoot user) @@ -169,7 +438,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService var claims = new List { new(AbpClaimTypes.UserId, user.Id.ToString()), - new(AbpClaimTypes.UserName, user.UserName) + new(AbpClaimTypes.UserName, user.UserName), + new(UsAppJwtClaims.ClientKind, UsAppJwtClaims.ClientKindUsApp) }; if (!string.IsNullOrWhiteSpace(user.Email)) @@ -257,4 +527,22 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService return segments.Count == 0 ? "无" : string.Join(", ", segments); } + + private sealed class MyProfileRoleRow + { + public string? RoleName { get; set; } + + public string? RoleCode { get; set; } + } + + private sealed class LocationManagerRow + { + public string? Name { get; set; } + + public string? Nick { get; set; } + + public string? UserName { get; set; } + + public long? Phone { get; set; } + } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs index 1a89a0d..03a6822 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs +++ b/美国版/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 /// 获取当前门店下四级嵌套数据 /// /// - /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category; + /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; + /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 + /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; + /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 /// [Authorize] public virtual async Task> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input) @@ -83,10 +86,15 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ LabelCategoryId = c.Id, LabelCategoryName = c.CategoryName, LabelCategoryPhotoUrl = c.CategoryPhotoUrl, + LabelCategoryButtonAppearance = c.ButtonAppearance, LabelCategoryOrderNum = c.OrderNum, ProductCategoryId = p.CategoryId, ProductCategoryName = pc.CategoryName, ProductCategoryPhotoUrl = pc.CategoryPhotoUrl, + ProductCategoryDisplayText = pc.DisplayText, + ProductCategoryButtonAppearance = pc.ButtonAppearance, + ProductCategoryAvailabilityType = pc.AvailabilityType, + ProductCategoryOrderNum = pc.OrderNum, ProductId = p.Id, ProductName = p.ProductName, ProductCode = p.ProductCode, @@ -112,17 +120,22 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ x.LabelCategoryId, x.LabelCategoryName, x.LabelCategoryPhotoUrl, + x.LabelCategoryButtonAppearance, x.LabelCategoryOrderNum }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName); var result = new List(); foreach (var g1 in byL1) { + var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance) + ? "TEXT" + : g1.Key.LabelCategoryButtonAppearance.Trim(); var l1 = new UsAppLabelCategoryTreeNodeDto { Id = g1.Key.LabelCategoryId, CategoryName = g1.Key.LabelCategoryName ?? string.Empty, CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl, + ButtonAppearance = l1Appearance, OrderNum = g1.Key.LabelCategoryOrderNum, ProductCategories = new List() }; @@ -136,7 +149,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ { CategoryId = (string?)null, CategoryName = "无", - CategoryPhotoUrl = (string?)null + CategoryPhotoUrl = (string?)null, + DisplayText = (string?)null, + ButtonAppearance = (string?)null, + AvailabilityType = (string?)null, + CategoryOrderNum = int.MaxValue }; } @@ -146,19 +163,34 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ { CategoryId = (string?)categoryId, CategoryName = categoryName, - CategoryPhotoUrl = categoryPhotoUrl + CategoryPhotoUrl = categoryPhotoUrl, + DisplayText = NormalizeNullableUrl(x.ProductCategoryDisplayText), + ButtonAppearance = NormalizeNullableId(x.ProductCategoryButtonAppearance), + AvailabilityType = NormalizeNullableId(x.ProductCategoryAvailabilityType), + CategoryOrderNum = x.ProductCategoryOrderNum }; }) - .OrderBy(g => g.Key.CategoryName); + .OrderBy(g => g.Key.CategoryOrderNum) + .ThenBy(g => g.Key.CategoryName); foreach (var g2 in byL2) { var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); + var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance) + ? "TEXT" + : g2.Key.ButtonAppearance.Trim(); + var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType) + ? "ALL" + : g2.Key.AvailabilityType.Trim().ToUpperInvariant(); var l2 = new UsAppProductCategoryNodeDto { CategoryId = g2.Key.CategoryId, CategoryPhotoUrl = g2.Key.CategoryPhotoUrl, Name = g2.Key.CategoryName, + DisplayText = g2.Key.DisplayText, + ButtonAppearance = appearance, + AvailabilityType = availability, + OrderNum = g2.Key.CategoryOrderNum == int.MaxValue ? 0 : g2.Key.CategoryOrderNum, ItemCount = productsGrouped.Count(), Products = new List() }; @@ -424,7 +456,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ } var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId); - var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value); + var normalizedPrintInput = ParsePrintInputJsonToDictionary(input.PrintInputJson); // 解析模板 elements(与预览一致的渲染数据) var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo @@ -581,8 +613,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ throw new UserFriendlyException("打印任务不存在"); } - // 仅允许重打自己在当前门店的任务 - if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) + // 非 admin:仅允许重打自己在当前门店的任务;admin 可重打任意用户任务(仍须门店一致) + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + if (!isAdmin && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) { throw new UserFriendlyException("无权限重打该任务"); } @@ -852,14 +885,29 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) - .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) + .Where((lp, l, p, c, t, tpl, pc) => + !c.IsDeleted && c.State && + (c.AvailabilityType == "ALL" || + (c.AvailabilityType == "SPECIFIED" && + SqlFunc.Subqueryable() + .Where(loc => loc.CategoryId == c.Id && loc.LocationId == locationId) + .Any()))) .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) + .Where((lp, l, p, c, t, tpl, pc) => + pc.Id == null || + (!pc.IsDeleted && pc.State && + (pc.AvailabilityType == "ALL" || + (pc.AvailabilityType == "SPECIFIED" && + SqlFunc.Subqueryable() + .Where(loc => loc.CategoryId == pc.Id && loc.LocationId == locationId) + .Any())))) .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => (l.LabelName != null && l.LabelName.Contains(keyword!)) || (p.ProductName != null && p.ProductName.Contains(keyword!)) || (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) || + (pc.DisplayText != null && pc.DisplayText.Contains(keyword!)) || (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || (t.TypeName != null && t.TypeName.Contains(keyword!)) || (l.LabelCode != null && l.LabelCode.Contains(keyword!))); @@ -875,6 +923,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ public string? LabelCategoryPhotoUrl { get; set; } + public string? LabelCategoryButtonAppearance { get; set; } + public int LabelCategoryOrderNum { get; set; } public string? ProductCategoryId { get; set; } @@ -883,6 +933,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ public string? ProductCategoryPhotoUrl { get; set; } + public string? ProductCategoryDisplayText { get; set; } + + public string? ProductCategoryButtonAppearance { get; set; } + + public string? ProductCategoryAvailabilityType { get; set; } + + public int ProductCategoryOrderNum { get; set; } + public string ProductId { get; set; } = string.Empty; public string? ProductName { get; set; } @@ -908,6 +966,32 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ public string TemplateUnit { get; set; } = "inch"; } + /// + /// 将 App 入参中的 JsonElement(对象或 null)反序列化为 PreviewAsync 所需的扁平字典。 + /// + private static Dictionary? ParsePrintInputJsonToDictionary(JsonElement? printInputJson) + { + if (printInputJson is null) + { + return null; + } + + var je = printInputJson.Value; + if (je.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + return null; + } + + try + { + return JsonSerializer.Deserialize>(je.GetRawText()); + } + catch + { + return null; + } + } + private static string NormalizeCategoryName(string? categoryName) { var s = categoryName?.Trim(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql new file mode 100644 index 0000000..fa5c45a --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql @@ -0,0 +1,21 @@ +-- 组织/分组(Group),归属合作伙伴(Parent Partner) +-- 依赖:请先执行 fl_partner_create.sql,保证存在 fl_partner 表。 + +CREATE TABLE IF NOT EXISTS `fl_group` ( + `Id` varchar(64) NOT NULL COMMENT '主键', + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', + `GroupName` varchar(256) NOT NULL COMMENT '组织名称', + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id', + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + PRIMARY KEY (`Id`), + KEY `IX_fl_group_IsDeleted` (`IsDeleted`), + KEY `IX_fl_group_State` (`State`), + KEY `IX_fl_group_PartnerId` (`PartnerId`), + KEY `IX_fl_group_GroupName` (`GroupName`(128)), + KEY `IX_fl_group_CreationTime` (`CreationTime`), + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)'; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql new file mode 100644 index 0000000..a0c37fd --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql @@ -0,0 +1,8 @@ +-- 从旧版「按门店」结构迁移为「全局一条」:删除 LocationId 及门店唯一索引 +-- 执行前请确认库中 `fl_location_support` 已存在;若不存在可跳过本脚本,直接使用 fl_location_support_create.sql + +-- 若存在按门店的唯一索引则删除(名称与历史脚本一致) +ALTER TABLE `fl_location_support` DROP INDEX `uk_fl_location_support_locationid`; + +-- 删除门店列(若列不存在会报错,需按环境调整) +ALTER TABLE `fl_location_support` DROP COLUMN `LocationId`; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql new file mode 100644 index 0000000..2c5b93a --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql @@ -0,0 +1,13 @@ +-- 全局 Support 联系方式(全门店共用一条) +CREATE TABLE IF NOT EXISTS `fl_location_support` ( + `Id` varchar(50) NOT NULL COMMENT '主键', + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', + `CreationTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `CreatorId` varchar(50) DEFAULT NULL COMMENT '创建人', + `LastModifierId` varchar(50) DEFAULT NULL COMMENT '最后修改人', + `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间', + `SupportPhone` varchar(100) NOT NULL COMMENT 'Support 电话', + `SupportEmail` varchar(200) NOT NULL COMMENT 'Support 邮箱', + PRIMARY KEY (`Id`), + KEY `idx_fl_location_support_isdeleted` (`IsDeleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='全局 Support 联系方式'; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql new file mode 100644 index 0000000..e4dee86 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql @@ -0,0 +1,20 @@ +-- 合作伙伴主数据(美国版 antis-foodlabeling-us) +-- 执行前请确认连接库为业务库;若表已存在请勿重复执行。 + +CREATE TABLE IF NOT EXISTS `fl_partner` ( + `Id` varchar(64) NOT NULL COMMENT '主键', + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', + `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称', + `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱', + `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话', + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + PRIMARY KEY (`Id`), + KEY `IX_fl_partner_IsDeleted` (`IsDeleted`), + KEY `IX_fl_partner_State` (`State`), + KEY `IX_fl_partner_CreationTime` (`CreationTime`), + KEY `IX_fl_partner_PartnerName` (`PartnerName`(128)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合作伙伴'; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/IServices/IAccountService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/IServices/IAccountService.cs index e06151a..e51810e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/IServices/IAccountService.cs +++ b/美国版/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 Task RestPasswordAsync(Guid userId, RestPasswordDto input); /// - /// 提供其他服务使用,根据用户id,直接返回token - /// - /// - Task PostLoginAsync(Guid userId); - - /// /// 根据信息查询用户,可能为空,代表该用户不存在或禁用 /// /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/ObjectMapping/RbacMapsterRegister.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/ObjectMapping/RbacMapsterRegister.cs new file mode 100644 index 0000000..bbeeb64 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/ObjectMapping/RbacMapsterRegister.cs @@ -0,0 +1,26 @@ +using Mapster; +using Yi.Framework.Rbac.Application.Contracts.Dtos.Menu; +using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.Rbac.Domain.Shared; +using Yi.Framework.Rbac.Domain.Shared.Dtos; + +namespace Yi.Framework.Rbac.Application.ObjectMapping; + +/// +/// Menu.ParentId 实体为字符串,对外 DTO 仍为 。 +/// +public class RbacMapsterRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId)); + + config.NewConfig() + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId)); + + // 登录组装用户信息时使用;库中 ParentId 可能为 "0",不能交给 Mapster 默认 string→Guid + config.NewConfig() + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId)); + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs index a045d6e..2b58f5a 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; +using SqlSugar; using Volo.Abp.Application.Services; using Volo.Abp.Authorization; using Volo.Abp.Caching; @@ -83,7 +84,7 @@ namespace Yi.Framework.Rbac.Application.Services //登录不想要验证码 ,可不校验 if (!_captcha.Validate(uuid, code)) { - throw new UserFriendlyException("验证码错误"); + throw new UserFriendlyException("Invalid captcha."); } } } @@ -97,21 +98,52 @@ namespace Yi.Framework.Rbac.Application.Services [AllowAnonymous] public async Task PostLoginAsync(LoginInputVo input) { - if (string.IsNullOrEmpty(input.Password) || string.IsNullOrEmpty(input.UserName)) + var email = input.UserName?.Trim(); + var password = input.Password ?? string.Empty; + if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password)) { - throw new UserFriendlyException("请输入合理数据!"); + throw new UserFriendlyException("Email and password are required."); + } + + if (!IsPlausibleEmail(email)) + { + // Platform sign-in supports email only. + throw new UserFriendlyException("Sign-in failed: email is required as the account."); } //校验验证码 ValidationImageCaptcha(input.Uuid,input.Code); - UserAggregateRoot user = new(); - //校验 - await _accountManager.LoginValidationAsync(input.UserName, input.Password, x => user = x); + var normalized = email.ToLowerInvariant(); + // 平台登录框为「邮箱」:优先按 Email 匹配;若历史账号仅把邮箱形字符串写在 UserName、Email 为空,则按 UserName 匹配。 + var candidates = await _userRepository._DbQueryable + .Where(u => !u.IsDeleted && u.State == true) + .Where(u => + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) || + SqlFunc.ToLower(u.UserName) == normalized) + .ToListAsync(); + var user = candidates.FirstOrDefault(u => + u.Email != null && + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase)) + ?? candidates.FirstOrDefault(); + if (user is null) + { + throw new UserFriendlyException("Sign-in failed: account not found."); + } + + if (!user.JudgePassword(password)) + { + throw new UserFriendlyException("Sign-in failed: incorrect email or password."); + } return await PostLoginAsync(user.Id); } + private static bool IsPlausibleEmail(string email) => + email.Contains("@", StringComparison.Ordinal) && + !email.StartsWith("@", StringComparison.Ordinal) && + !email.EndsWith("@", StringComparison.Ordinal); + /// /// 提供其他服务使用,根据用户id,直接返回token diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/MenuParentIdConverter.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/MenuParentIdConverter.cs new file mode 100644 index 0000000..77f2a96 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/MenuParentIdConverter.cs @@ -0,0 +1,26 @@ +namespace Yi.Framework.Rbac.Domain.Shared; + +/// +/// Menu.ParentId 在部分库中为 varchar(如根节点为 0),与 ORM 中 Guid 映射不一致时会导致 SqlSugar 绑定失败;统一用字符串落库并在需要时转为 。 +/// +public static class MenuParentIdConverter +{ + public static bool IsRoot(string? raw) => + string.IsNullOrWhiteSpace(raw) || + raw.Trim() == "0" || + string.Equals(raw.Trim(), Guid.Empty.ToString(), StringComparison.OrdinalIgnoreCase); + + public static Guid ToGuid(string? raw) + { + if (IsRoot(raw)) + { + return Guid.Empty; + } + + var t = raw!.Trim(); + return Guid.TryParse(t, out var g) ? g : Guid.Empty; + } + + /// 写入数据库:根节点与历史库对齐为 0,否则为标准 GUID 字符串。 + public static string FromGuid(Guid g) => g == Guid.Empty ? "0" : g.ToString("D"); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/MenuAggregateRoot.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/MenuAggregateRoot.cs index 9ede281..9604c9b 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/MenuAggregateRoot.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/MenuAggregateRoot.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using System.Web; using NUglify.Helpers; using SqlSugar; @@ -7,6 +7,7 @@ using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; using Yi.Framework.Core.Data; using Yi.Framework.Core.Helper; +using Yi.Framework.Rbac.Domain.Shared; using Yi.Framework.Rbac.Domain.Shared.Dtos; using Yi.Framework.Rbac.Domain.Shared.Enums; @@ -25,13 +26,13 @@ namespace Yi.Framework.Rbac.Domain.Entities public MenuAggregateRoot(Guid id) { Id = id; - ParentId = Guid.Empty; + ParentId = MenuParentIdConverter.FromGuid(Guid.Empty); } public MenuAggregateRoot(Guid id, Guid parentId) { Id = id; - ParentId = parentId; + ParentId = MenuParentIdConverter.FromGuid(parentId); } /// @@ -100,8 +101,9 @@ namespace Yi.Framework.Rbac.Domain.Entities /// /// /// + /// 父级菜单 Id;库中多为 varchar(如根为 0 或 GUID 文本)。 [SugarColumn(ColumnName = "ParentId")] - public Guid ParentId { get; set; } + public string ParentId { get; set; } = "0"; /// /// 菜单图标 @@ -183,7 +185,7 @@ namespace Yi.Framework.Rbac.Domain.Entities r.OrderNum = m.OrderNum; var routerName = m.Router?.Split("/").LastOrDefault(); r.Id = m.Id; - r.ParentId = m.ParentId; + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId); //开头大写 r.Name = routerName?.First().ToString().ToUpper() + routerName?.Substring(1); @@ -197,7 +199,7 @@ namespace Yi.Framework.Rbac.Domain.Entities r.AlwaysShow = true; //判断是否为最顶层的路由 - if (Guid.Empty == m.ParentId) + if (MenuParentIdConverter.IsRoot(m.ParentId)) { r.Component = "Layout"; } @@ -250,7 +252,7 @@ namespace Yi.Framework.Rbac.Domain.Entities var r = new Vue3RouterDto(); r.OrderNum = m.OrderNum; r.Id = m.Id; - r.ParentId = m.ParentId; + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId); r.Hidden = !m.IsShow; // 检测是否为 URL 链接(http:// 或 https:// 开头) @@ -359,7 +361,7 @@ namespace Yi.Framework.Rbac.Domain.Entities r.AlwaysShow = false; // 判断是否为最顶层的路由 - if (Guid.Empty == m.ParentId) + if (MenuParentIdConverter.IsRoot(m.ParentId)) { r.Component = "Layout"; } @@ -385,7 +387,7 @@ namespace Yi.Framework.Rbac.Domain.Entities r.AlwaysShow = true; // 判断是否为最顶层的路由 - if (Guid.Empty == m.ParentId) + if (MenuParentIdConverter.IsRoot(m.ParentId)) { r.Component = "Layout"; } @@ -449,7 +451,7 @@ namespace Yi.Framework.Rbac.Domain.Entities }, Children =null, Id = m.Id, - ParentId = m.ParentId + ParentId = MenuParentIdConverter.ToGuid(m.ParentId) }) .ToList(); @@ -487,7 +489,7 @@ namespace Yi.Framework.Rbac.Domain.Entities var treeDto = new MenuTreeDto { Id = m.Id, - ParentId = m.ParentId, + ParentId = MenuParentIdConverter.ToGuid(m.ParentId), OrderNum = m.OrderNum, MenuName = m.MenuName, MenuType = m.MenuType, diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs index f2b393e..1a9193b 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs @@ -1,4 +1,4 @@ -using System.IdentityModel.Tokens.Jwt; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Mapster; @@ -77,10 +77,8 @@ namespace Yi.Framework.Rbac.Domain.Managers { throw new UserFriendlyException(UserConst.No_Role); } - if (!userInfo.PermissionCodes.Any()) - { - throw new UserFriendlyException(UserConst.No_Permission); - } + + // 菜单表 PermissionCode 可未落库;当前以角色及角色-菜单绑定为准,不要求 PermissionCodes 非空 if (getUserInfo is not null) { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/UserManager.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/UserManager.cs index cb05705..d826efa 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/UserManager.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/UserManager.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Mapster; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +10,7 @@ using Volo.Abp.EventBus.Local; using Volo.Abp.Guids; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.Rbac.Domain.Repositories; +using Yi.Framework.Rbac.Domain.Shared; using Yi.Framework.Rbac.Domain.Shared.Caches; using Yi.Framework.Rbac.Domain.Shared.Consts; using Yi.Framework.Rbac.Domain.Shared.Dtos; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuPureDataSeed.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuPureDataSeed.cs index 3eaf7d6..7365854 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuPureDataSeed.cs +++ b/美国版/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 Router = "/system/user/index", MenuIcon = "ri:admin-line", OrderNum = 100, - ParentId = system.Id, + ParentId = system.Id.ToString(), RouterName = "SystemUser" }; entities.Add(user); @@ -99,7 +99,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userQuery); @@ -111,7 +111,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userAdd); @@ -123,7 +123,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userEdit); @@ -135,7 +135,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userRemove); @@ -148,7 +148,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:resetPwd", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userResetPwd); @@ -164,7 +164,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Router = "/system/role/index", MenuIcon = "ri:admin-fill", OrderNum = 99, - ParentId = system.Id, + ParentId = system.Id.ToString(), RouterName = "SystemRole" }; entities.Add(role); @@ -176,7 +176,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleQuery); @@ -188,7 +188,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleAdd); @@ -200,7 +200,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleEdit); @@ -212,7 +212,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleRemove); @@ -228,7 +228,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Router = "/system/menu/index", MenuIcon = "ep:menu", OrderNum = 98, - ParentId = system.Id, + ParentId = system.Id.ToString(), RouterName = "SystemMenu" }; entities.Add(menu); @@ -240,7 +240,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuQuery); @@ -252,7 +252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuAdd); @@ -264,7 +264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuEdit); @@ -276,7 +276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuRemove); @@ -291,7 +291,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Router = "/system/dept/index", MenuIcon = "ri:git-branch-line", OrderNum = 97, - ParentId = system.Id, + ParentId = system.Id.ToString(), RouterName = "SystemDept" }; entities.Add(dept); @@ -303,7 +303,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptQuery); @@ -315,7 +315,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptAdd); @@ -327,7 +327,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptEdit); @@ -339,7 +339,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptRemove); @@ -356,7 +356,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Router = "/system/post/index", MenuIcon = "ant-design:deployment-unit-outlined", OrderNum = 96, - ParentId = system.Id, + ParentId = system.Id.ToString(), RouterName = "SystemPost" }; entities.Add(post); @@ -368,7 +368,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postQuery); @@ -380,7 +380,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postAdd); @@ -392,7 +392,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postEdit); @@ -404,7 +404,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postRemove); @@ -420,7 +420,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Router = "/monitor/operation-logs", MenuIcon = "ri:history-fill", OrderNum = 100, - ParentId = monitoring.Id, + ParentId = monitoring.Id.ToString(), RouterName = "OperationLog", Component = "monitor/logs/operation/index" }; @@ -433,7 +433,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:operlog:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = operationLog.Id, + ParentId = operationLog.Id.ToString(), IsDeleted = false }; entities.Add(operationLogQuery); @@ -445,7 +445,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:operlog:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = operationLog.Id, + ParentId = operationLog.Id.ToString(), IsDeleted = false }; entities.Add(operationLogRemove); @@ -465,7 +465,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "monitor/logs/login/index", MenuIcon = "ri:window-line", OrderNum = 100, - ParentId = monitoring.Id, + ParentId = monitoring.Id.ToString(), RouterName = "LoginLog", }; entities.Add(loginLog); @@ -477,7 +477,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:logininfor:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = loginLog.Id, + ParentId = loginLog.Id.ToString(), IsDeleted = false }; entities.Add(loginLogQuery); @@ -489,7 +489,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:logininfor:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = loginLog.Id, + ParentId = loginLog.Id.ToString(), IsDeleted = false, }; @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "/system/config/index", MenuIcon = "ri:edit-box-line", OrderNum = 94, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(config); @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configQuery); @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configAdd); @@ -545,7 +545,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configEdit); @@ -557,7 +557,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configRemove); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuRuoYiDataSeed.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuRuoYiDataSeed.cs index f00d25c..eb024b2 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuRuoYiDataSeed.cs +++ b/美国版/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 Component = "code/field/index", MenuIcon = "number", OrderNum = 99, - ParentId = code.Id, + ParentId = code.Id.ToString(), IsDeleted = false }; entities.Add(field); @@ -599,7 +599,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/tenant/index", MenuIcon = "list", OrderNum = 101, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(tenant); @@ -611,7 +611,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantQuery); @@ -623,7 +623,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantAdd); @@ -635,7 +635,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantEdit); @@ -647,7 +647,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantRemove); @@ -677,7 +677,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/user/index", MenuIcon = "user", OrderNum = 100, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(user); @@ -689,7 +689,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userQuery); @@ -701,7 +701,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userAdd); @@ -713,7 +713,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userEdit); @@ -725,7 +725,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userRemove); @@ -738,7 +738,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:resetPwd", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userResetPwd); @@ -758,7 +758,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/role/index", MenuIcon = "peoples", OrderNum = 99, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(role); @@ -770,7 +770,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleQuery); @@ -782,7 +782,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleAdd); @@ -794,7 +794,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleEdit); @@ -806,7 +806,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleRemove); @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/menu/index", MenuIcon = "tree-table", OrderNum = 98, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(menu); @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuQuery); @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuAdd); @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuEdit); @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuRemove); @@ -893,7 +893,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/dept/index", MenuIcon = "tree", OrderNum = 97, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(dept); @@ -905,7 +905,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptQuery); @@ -917,7 +917,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptAdd); @@ -929,7 +929,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptEdit); @@ -941,7 +941,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptRemove); @@ -962,7 +962,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/post/index", MenuIcon = "post", OrderNum = 96, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(post); @@ -974,7 +974,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postQuery); @@ -986,7 +986,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postAdd); @@ -998,7 +998,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postEdit); @@ -1010,7 +1010,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postRemove); @@ -1029,7 +1029,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/dict/index", MenuIcon = "dict", OrderNum = 95, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(dict); @@ -1041,7 +1041,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictQuery); @@ -1053,7 +1053,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictAdd); @@ -1065,7 +1065,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictEdit); @@ -1077,7 +1077,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictRemove); @@ -1097,7 +1097,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/config/index", MenuIcon = "edit", OrderNum = 94, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(config); @@ -1109,7 +1109,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configQuery); @@ -1121,7 +1121,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configAdd); @@ -1133,7 +1133,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configEdit); @@ -1145,7 +1145,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configRemove); @@ -1167,7 +1167,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/notice/index", MenuIcon = "message", OrderNum = 93, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(notice); @@ -1179,7 +1179,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeQuery); @@ -1191,7 +1191,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeAdd); @@ -1203,7 +1203,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeEdit); @@ -1215,7 +1215,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeRemove); @@ -1233,7 +1233,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds IsLink = false, MenuIcon = "log", OrderNum = 92, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(log); @@ -1252,7 +1252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "monitor/operlog/index", MenuIcon = "form", OrderNum = 100, - ParentId = log.Id, + ParentId = log.Id.ToString(), IsDeleted = false }; entities.Add(operationLog); @@ -1264,7 +1264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:operlog:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = operationLog.Id, + ParentId = operationLog.Id.ToString(), IsDeleted = false }; entities.Add(operationLogQuery); @@ -1276,7 +1276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:operlog:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = operationLog.Id, + ParentId = operationLog.Id.ToString(), IsDeleted = false }; entities.Add(operationLogRemove); @@ -1296,7 +1296,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "monitor/logininfor/index", MenuIcon = "logininfor", OrderNum = 100, - ParentId = log.Id, + ParentId = log.Id.ToString(), IsDeleted = false }; entities.Add(loginLog); @@ -1308,7 +1308,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:logininfor:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = loginLog.Id, + ParentId = loginLog.Id.ToString(), IsDeleted = false }; entities.Add(loginLogQuery); @@ -1320,7 +1320,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:logininfor:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = loginLog.Id, + ParentId = loginLog.Id.ToString(), IsDeleted = false }; entities.Add(loginLogRemove); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuVben5DataSeed.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuVben5DataSeed.cs index f6437dd..7db6b19 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuVben5DataSeed.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuVben5DataSeed.cs @@ -1,4 +1,4 @@ -using Volo.Abp.Data; +using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; using Yi.Framework.Rbac.Domain.Entities; @@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds // Component = "code/field/index", // MenuIcon = "tabler:file-code", // OrderNum = 99, - // ParentId = code.Id, + // ParentId = code.Id.ToString(), // IsDeleted = false // }; // entities.Add(field); @@ -251,7 +251,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/tenant/index", MenuIcon = "tabler:users", OrderNum = 101, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(tenant); @@ -263,7 +263,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantQuery); @@ -275,7 +275,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantAdd); @@ -287,7 +287,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantEdit); @@ -299,7 +299,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:tenant:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = tenant.Id, + ParentId = tenant.Id.ToString(), IsDeleted = false }; entities.Add(tenantRemove); @@ -318,7 +318,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/user/index", MenuIcon = "tabler:user", OrderNum = 100, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(user); @@ -330,7 +330,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userQuery); @@ -342,7 +342,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userAdd); @@ -354,7 +354,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userEdit); @@ -366,7 +366,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userRemove); @@ -379,7 +379,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:user:resetPwd", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = user.Id, + ParentId = user.Id.ToString(), IsDeleted = false }; entities.Add(userResetPwd); @@ -399,7 +399,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/role/index", MenuIcon = "eos-icons:role-binding-outlined", OrderNum = 99, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(role); @@ -411,7 +411,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleQuery); @@ -423,7 +423,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleAdd); @@ -435,7 +435,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleEdit); @@ -447,7 +447,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:role:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = role.Id, + ParentId = role.Id.ToString(), IsDeleted = false }; entities.Add(roleRemove); @@ -466,7 +466,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds MenuIcon = "tabler:user-shield", OrderNum = 15, IsDeleted = false, - ParentId = system.Id + ParentId = system.Id.ToString() }; entities.Add(roleAuthUser); @@ -485,7 +485,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/menu/index", MenuIcon = "ic:sharp-menu", OrderNum = 98, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(menu); @@ -497,7 +497,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuQuery); @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuAdd); @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuEdit); @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:menu:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = menu.Id, + ParentId = menu.Id.ToString(), IsDeleted = false }; entities.Add(menuRemove); @@ -552,7 +552,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/dept/index", MenuIcon = "mingcute:department-line", OrderNum = 97, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(dept); @@ -564,7 +564,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptQuery); @@ -576,7 +576,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptAdd); @@ -588,7 +588,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptEdit); @@ -600,7 +600,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dept:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dept.Id, + ParentId = dept.Id.ToString(), IsDeleted = false }; entities.Add(deptRemove); @@ -621,7 +621,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/post/index", MenuIcon = "tabler:user-star", OrderNum = 96, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(post); @@ -633,7 +633,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postQuery); @@ -645,7 +645,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postAdd); @@ -657,7 +657,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postEdit); @@ -669,7 +669,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:post:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = post.Id, + ParentId = post.Id.ToString(), IsDeleted = false }; entities.Add(postRemove); @@ -688,7 +688,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/dict/index", MenuIcon = "fluent-mdl2:dictionary", OrderNum = 95, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(dict); @@ -700,7 +700,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictQuery); @@ -712,7 +712,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictAdd); @@ -724,7 +724,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictEdit); @@ -736,7 +736,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:dict:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = dict.Id, + ParentId = dict.Id.ToString(), IsDeleted = false }; entities.Add(dictRemove); @@ -756,7 +756,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/config/index", MenuIcon = "ant-design:setting-outlined", OrderNum = 94, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(config); @@ -768,7 +768,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configQuery); @@ -780,7 +780,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configAdd); @@ -792,7 +792,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configEdit); @@ -804,7 +804,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:config:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = config.Id, + ParentId = config.Id.ToString(), IsDeleted = false }; entities.Add(configRemove); @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "system/notice/index", MenuIcon = "fe:notice-push", OrderNum = 93, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(notice); @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeQuery); @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:add", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeAdd); @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:edit", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeEdit); @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "system:notice:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = notice.Id, + ParentId = notice.Id.ToString(), IsDeleted = false }; entities.Add(noticeRemove); @@ -892,7 +892,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds IsLink = false, MenuIcon = "material-symbols:logo-dev-outline", OrderNum = 92, - ParentId = system.Id, + ParentId = system.Id.ToString(), IsDeleted = false }; entities.Add(log); @@ -911,7 +911,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "monitor/operlog/index", MenuIcon = "tabler:align-box-right-middle", OrderNum = 100, - ParentId = log.Id, + ParentId = log.Id.ToString(), IsDeleted = false }; entities.Add(operationLog); @@ -923,7 +923,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:operlog:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = operationLog.Id, + ParentId = operationLog.Id.ToString(), IsDeleted = false }; entities.Add(operationLogQuery); @@ -935,7 +935,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:operlog:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = operationLog.Id, + ParentId = operationLog.Id.ToString(), IsDeleted = false }; entities.Add(operationLogRemove); @@ -955,7 +955,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds Component = "monitor/logininfor/index", MenuIcon = "tabler:align-box-right-middle", OrderNum = 100, - ParentId = log.Id, + ParentId = log.Id.ToString(), IsDeleted = false }; entities.Add(loginLog); @@ -967,7 +967,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:logininfor:query", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = loginLog.Id, + ParentId = loginLog.Id.ToString(), IsDeleted = false }; entities.Add(loginLogQuery); @@ -979,7 +979,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds PermissionCode = "monitor:logininfor:remove", MenuType = MenuTypeEnum.Component, OrderNum = 100, - ParentId = loginLog.Id, + ParentId = loginLog.Id.ToString(), IsDeleted = false }; entities.Add(loginLogRemove); diff --git a/项目相关文档/Dashboard统计接口对接说明.md b/项目相关文档/Dashboard统计接口对接说明.md new file mode 100644 index 0000000..af28dbc --- /dev/null +++ b/项目相关文档/Dashboard统计接口对接说明.md @@ -0,0 +1,247 @@ +# Dashboard 统计接口对接说明 + +> 适用范围:美国版 Web 管理端 Dashboard 首页统计 +> +> 接口实现:`IDashboardAppService.GetOverviewAsync` / `DashboardAppService.GetOverviewAsync` + +--- + +## 1. 接口信息 + +- **方法**:`GET` +- **路径**:`/api/app/dashboard/overview` +- **鉴权**:需要登录(Bearer Token) +- **请求参数**:无 + +--- + +## 2. 返回结构(顶层) + +```json +{ + "labelsPrintedToday": {}, + "activeTemplates": {}, + "activeUsers": {}, + "locations": {}, + "people": {}, + "products": {}, + "weeklyPrintVolume": [], + "byCategory": [], + "byCategoryTotal": 0, + "recentLabels": [], + "generatedAt": "2026-04-22T10:00:00+08:00", + + "metricCards": [], + "categoryDistribution": [], + "categoryDistributionTotal": 0 +} +``` + +说明: +- `labelsPrintedToday/activeTemplates/...`、`byCategory/byCategoryTotal` 是**前端直观命名**(推荐使用)。 +- `metricCards`、`categoryDistribution`、`categoryDistributionTotal` 为**兼容字段**(与旧版返回一致)。 +- `recentLabels`:**Recent Labels** 区块数据,全门店按打印时间倒序取最新 **10** 条(`fl_label_print_task`)。 + +--- + +## 3. 字段说明 + +### 3.1 指标卡片对象(`DashboardMetricCardDto`) + +用于以下字段: +- `labelsPrintedToday` +- `activeTemplates` +- `activeUsers` +- `locations` +- `people` +- `products` +- `metricCards[]`(同结构) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `key` | string | 指标标识(如 `labelsPrintedToday`) | +| `title` | string | 指标标题 | +| `value` | int | 当前值 | +| `previousValue` | int | 对比周期值 | +| `changeValue` | int | 增减值(`value - previousValue`) | +| `changeRate` | decimal | 增减比例(百分比,保留 2 位) | + +--- + +### 3.2 周趋势(`weeklyPrintVolume`) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `date` | string | 日期,格式 `yyyy-MM-dd` | +| `value` | int | 当天打印量 | + +--- + +### 3.3 分类分布(`byCategory`) + +`byCategory` 与 `categoryDistribution` 结构一致。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `categoryId` | string | 分类 Id(`fl_label_category.Id`) | +| `categoryName` | string | 分类名称 | +| `count` | int | 该分类下标签数量 | +| `ratio` | decimal | 占比(百分比,保留 2 位) | + +--- + +### 3.4 最近打印标签(`recentLabels`) + +数组元素类型:`DashboardRecentLabelItemDto`,按 **`PrintedAt`(`PrintedAt` 为空则用 `CreationTime`)倒序**,最多 **10** 条。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `taskId` | string | 打印任务 Id(`fl_label_print_task.Id`) | +| `labelCode` | string | 标签编码(界面 Serial,如 `1-251201`) | +| `displayName` | string | 展示标题:优先 **产品名**,否则 **标签名** | +| `printedByUserId` | string \| null | 打印人用户 Id(`CreatedBy`) | +| `printedByName` | string | 打印人展示名(`User.Name` 或 `UserName`) | +| `printedAt` | string (datetime) | 打印时间(ISO 8601) | +| `status` | string | `active` 或 `expired`:从 `PrintInputJson` 解析 `expiryDate` / `expiry` / `expirationDate`,与**当天日期**比较;无保质期或解析失败视为 `active` | +| `labelTypeBadge` | string | 模板尺寸短文案(如 `2"x2"`),用于左侧圆标 | + +前端可用 `printedAt` 自行格式化为「10 mins ago」等相对时间。 + +--- + +## 4. 返回示例 + +```json +{ + "labelsPrintedToday": { + "key": "labelsPrintedToday", + "title": "Labels Printed Today", + "value": 342, + "previousValue": 305, + "changeValue": 37, + "changeRate": 12.13 + }, + "activeTemplates": { + "key": "activeTemplates", + "title": "Active Templates", + "value": 24, + "previousValue": 22, + "changeValue": 2, + "changeRate": 9.09 + }, + "activeUsers": { + "key": "activeUsers", + "title": "Active Users", + "value": 8, + "previousValue": 7, + "changeValue": 1, + "changeRate": 14.29 + }, + "locations": { + "key": "locations", + "title": "Locations", + "value": 12, + "previousValue": 11, + "changeValue": 1, + "changeRate": 9.09 + }, + "people": { + "key": "people", + "title": "People", + "value": 48, + "previousValue": 45, + "changeValue": 3, + "changeRate": 6.67 + }, + "products": { + "key": "products", + "title": "Products", + "value": 156, + "previousValue": 156, + "changeValue": 0, + "changeRate": 0 + }, + "weeklyPrintVolume": [ + { "date": "2026-04-16", "value": 142 }, + { "date": "2026-04-17", "value": 226 }, + { "date": "2026-04-18", "value": 185 }, + { "date": "2026-04-19", "value": 261 }, + { "date": "2026-04-20", "value": 192 }, + { "date": "2026-04-21", "value": 121 }, + { "date": "2026-04-22", "value": 342 } + ], + "byCategory": [ + { "categoryId": "CAT001", "categoryName": "Breakfast", "count": 420, "ratio": 42.00 }, + { "categoryId": "CAT002", "categoryName": "Lunch", "count": 350, "ratio": 35.00 }, + { "categoryId": "CAT003", "categoryName": "Dinner", "count": 230, "ratio": 23.00 } + ], + "byCategoryTotal": 1000, + "recentLabels": [ + { + "taskId": "…", + "labelCode": "1-251201", + "displayName": "Chicken Breast", + "printedByUserId": "…", + "printedByName": "Alice J.", + "printedAt": "2026-04-22T09:50:00+08:00", + "status": "active", + "labelTypeBadge": "2\"x2\"" + } + ], + "generatedAt": "2026-04-22T10:00:00+08:00", + "metricCards": [], + "categoryDistribution": [], + "categoryDistributionTotal": 1000 +} +``` + +--- + +## 5. 统计口径说明 + +### 5.1 Labels Printed Today +- 当前值:`fl_label_print_task` 在“今日 00:00~次日 00:00”的记录数。 +- 对比值:昨日同口径记录数。 + +### 5.2 Active Templates +- 当前值:`fl_label_template` 中 `IsDeleted = false AND State = true` 数量。 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 + +### 5.3 Active Users +- 当前值:`User` 表中 `IsDeleted = false AND State = true` 数量。 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 + +### 5.4 Locations +- 当前值:`location` 表中 `IsDeleted = false` 数量。 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 + +### 5.5 People +- 当前值:`User` 表中 `IsDeleted = false` 数量。 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。 + +### 5.6 Products +- 当前值:`fl_product` 表中 `IsDeleted = false` 数量。 +- 对比值:当前版本由于 `FlProductDbEntity` 未映射 `CreationTime`,临时按同口径总量返回(即变化可能为 0)。 + +### 5.7 Weekly Print Volume +- 统计最近 7 天(含今天)每天 `fl_label_print_task` 数量。 +- 无数据日期补 0。 + +### 5.8 By Category +- 基于启用且未删除的 `fl_label_category` 作为分类集合。 +- 统计 `fl_label` 中未删除且 `LabelCategoryId` 命中的数量。 +- 占比按 `count / byCategoryTotal * 100` 计算,保留 2 位。 + +--- + +## 6. 前端接入建议 + +- 新页面优先使用: + - 指标:`labelsPrintedToday`、`activeTemplates`、`activeUsers`、`locations`、`people`、`products` + - 图表:`weeklyPrintVolume` + - 环图:`byCategory` + `byCategoryTotal` +- 旧逻辑仍可使用兼容字段: + - `metricCards` + - `categoryDistribution` + - `categoryDistributionTotal` + diff --git a/项目相关文档/产品模块Categories接口对接说明.md b/项目相关文档/产品模块Categories接口对接说明.md index 37ad481..d548dee 100644 --- a/项目相关文档/产品模块Categories接口对接说明.md +++ b/项目相关文档/产品模块Categories接口对接说明.md @@ -8,7 +8,9 @@ - **接口前缀**:宿主统一前缀为 `/api/app` - **分类表**:`fl_product_category` - **关联字段**:`fl_product.category_id` → `fl_product_category.id` -- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) +- **外观字段(字符串落库,内容为 JSON 文本)**: + - `ButtonAppearance`(`buttonAppearance`):如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]`、或合法 JSON 对象/数组;兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时会规范为 JSON 数组,如 `["TEXT"]`)。 + - `CategoryPhotoUrl`(`categoryPhotoUrl`):与外观配合的**展示数据**,同样为 **JSON 字符串**(如 `["Prep","#10B981"]`、图片 URL 数组等);若传入**非 JSON** 的纯文本(如旧数据中的 `#EC4899` 或 `/picture/...`),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**库中字符串,由前端解析。 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 @@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi... | `id` | string | 主键 | | `categoryCode` | string | 类别编码 | | `categoryName` | string | 类别名称 | -| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) | +| `displayText` | string \| null | 按钮展示文案(空可回退 `categoryName`) | +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(含义由前端与 `buttonAppearance` 约定) | +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(见上文「外观字段」) | +| `availabilityType` | string | `ALL` / `SPECIFIED`(门店可用范围) | | `state` | boolean | 是否启用 | | `orderNum` | number | 排序 | | `lastEdited` | string | 最后编辑时间 | @@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi... "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", "categoryCode": "CAT_PREP", "categoryName": "Prep", - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "displayText": "Prep", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", + "availabilityType": "ALL", "state": true, "orderNum": 100, "lastEdited": "2026-03-25 12:30:10" @@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi... "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", "categoryCode": "CAT_PREP", "categoryName": "Prep", - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "displayText": "Prep", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", + "availabilityType": "ALL", + "locationIds": [], "state": true, "orderNum": 100 } @@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi... |------|------|------|------| | `categoryCode` | string | 是 | 类别编码(唯一) | | `categoryName` | string | 是 | 类别名称(唯一) | -| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) | +| `displayText` | string \| null | 否 | 按钮展示文案 | +| `categoryPhotoUrl` | string \| null | 否 | **JSON 字符串**;与 `buttonAppearance` 配合(见概述)。纯路径等非 JSON 文本会被后端包成 JSON 字符串存储。 | +| `buttonAppearance` | string | 否 | **JSON 字符串**;未传或空白时后端默认 `["TEXT"]`。兼容传 `TEXT`/`COLOR`/`IMAGE` 单行(会规范为 `["TEXT"]` 等)。非法非 JSON 且非上述三者时报错。 | +| `availabilityType` | string | 否 | `ALL`(默认)或 `SPECIFIED` | +| `locationIds` | string[] | 条件 | `availabilityType=SPECIFIED` 时必填且至少 1 个门店 Id | | `state` | boolean | 否 | 是否启用(默认 true) | | `orderNum` | number | 否 | 排序(默认 0) | @@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi... { "categoryCode": "CAT_PREP", "categoryName": "Prep", - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "displayText": "Prep", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "availabilityType": "ALL", + "locationIds": [], "state": true, "orderNum": 100 } @@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi... { "categoryCode": "CAT_PREP", "categoryName": "Prep", - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "displayText": "Prep", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "availabilityType": "ALL", + "locationIds": [], "state": true, "orderNum": 100 } @@ -179,7 +203,7 @@ Authorization: Bearer eyJhbGciOi... ### 约束 -- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。 +- 若该类别已被 `fl_product` 引用(`fl_product.CategoryId = id`),删除会失败并返回友好提示:`该类别已被产品引用,无法删除`。 ### 请求示例 @@ -200,5 +224,5 @@ Authorization: Bearer eyJhbGciOi... 推荐前端流程: 1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url` -2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url` +2. 新增/编辑类别时:若采用 **JSON** 存展示数据,将 `url` 写入你方约定的 JSON 结构(例如 `["IMAGE","/picture/..."]`);若仍传**纯路径字符串**,后端会将其序列化为 JSON 字符串再入库(与仅图片场景兼容)。 diff --git a/项目相关文档/合作伙伴Partner接口对接说明.md b/项目相关文档/合作伙伴Partner接口对接说明.md new file mode 100644 index 0000000..325e6dc --- /dev/null +++ b/项目相关文档/合作伙伴Partner接口对接说明.md @@ -0,0 +1,417 @@ +# 合作伙伴(Partner)与组织(Group)接口对接说明 + +> 适用范围:美国版 Web 管理端「Account Management」下的 **Partner**、**Group** 主数据 +> **Partner** 表:`fl_partner`,接口:`IPartnerAppService` / `PartnerAppService` +> **Group** 表:`fl_group`(`PartnerId` 关联 `fl_partner.Id`),接口:`IGroupAppService` / `GroupAppService` +> 宿主路由前缀:`/api/app`(与 `YiAbpWebModule` 中 `RootPath = api/app` 一致) + +--- + +## 0. 通用说明 + +- **鉴权**:需要登录(`Authorization: Bearer {token}`),与其它 `/api/app/*` 接口一致。 +- **Content-Type**:`POST` / `PUT` 使用 `application/json`。 +- **分页约定(美国版食品标签模块)**:`skipCount` 表示**页码(从 1 起)**,不是 0 基 offset;第一页请传 `skipCount=1`。与 `PagedQueryConvention` 及 SqlSugar `ToPageListAsync` 用法一致。 +- **逻辑删除**:`DELETE` 将对应表的 `IsDeleted` 置为 `true`(`fl_partner` / `fl_group`),不物理删行。 +- **列表与导出筛选一致**:各模块列表的筛选字段与对应 **export-pdf** 接口一致,便于数据对齐。 +- **Group 与 Partner**:新建/编辑 Group 时 `partnerId` 必须指向**未逻辑删除**的 `fl_partner`;列表左联 Partner 时仅展示未删除合作伙伴名称,已删 Partner 在列表中显示为 **`无`**。 + +--- + +# 第一部分:Partner(合作伙伴) + +## 1. 分页列表 + +- **方法**:`GET` +- **路径**:`/api/app/partner` + +### 1.1 查询参数(`PartnerGetListInputVo`) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `skipCount` | int | 是 | 页码,从 **1** 开始 | +| `maxResultCount` | int | 是 | 每页条数 | +| `sorting` | string | 否 | 排序,仅支持白名单(见下) | +| `keyword` | string | 否 | 模糊匹配 `PartnerName`、`ContactEmail`、`PhoneNumber` | +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 | + +**排序白名单**(大小写不敏感): + +- `PartnerName asc` / `PartnerName desc` +- `CreationTime asc` / `CreationTime desc` +- `State asc` / `State desc` + +其它值将回退为默认:**按 `CreationTime` 降序**。 + +### 1.2 请求示例 + +```http +GET /api/app/partner?skipCount=1&maxResultCount=10&keyword=Global&state=true&sorting=CreationTime%20desc HTTP/1.1 +Authorization: Bearer {token} +``` + +### 1.3 响应结构(`PagedResultWithPageDto`) + +```json +{ + "pageIndex": 1, + "pageSize": 10, + "totalCount": 2, + "totalPages": 1, + "items": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "partnerName": "Global Foods Inc.", + "contactEmail": "admin@globalfoods.com", + "phoneNumber": "+1 (555) 100-2000", + "state": true, + "creationTime": "2026-04-27T10:00:00" + } + ] +} +``` + +### 1.4 列表项字段(`PartnerGetListOutputDto`) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 主键 | +| `partnerName` | string | 合作伙伴名称 | +| `contactEmail` | string \| null | 联系邮箱 | +| `phoneNumber` | string \| null | 电话 | +| `state` | bool | 是否启用(UI Active 开关) | +| `creationTime` | string (datetime) | 创建时间 | + +--- + +## 2. 详情 + +- **方法**:`GET` +- **路径**:`/api/app/partner/{id}` + +### 2.1 路径参数 + +| 参数 | 说明 | +|------|------| +| `id` | `fl_partner.Id` | + +### 2.2 响应结构(`PartnerGetOutputDto`) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 主键 | +| `partnerName` | string | 合作伙伴名称 | +| `contactEmail` | string \| null | 联系邮箱 | +| `phoneNumber` | string \| null | 电话 | +| `state` | bool | 是否启用 | +| `creationTime` | string (datetime) | 创建时间 | +| `lastModificationTime` | string (datetime) \| null | 最后修改时间 | + +### 2.3 错误说明 + +- `id` 为空或记录不存在(含已逻辑删除):业务错误提示「合作伙伴不存在」等。 + +--- + +## 3. 新增 + +- **方法**:`POST` +- **路径**:`/api/app/partner` + +### 3.1 Body(`PartnerCreateInputVo`) + +```json +{ + "partnerName": "Global Foods Inc.", + "contactEmail": "admin@globalfoods.com", + "phoneNumber": "+1 (555) 100-2000", + "state": true +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `partnerName` | string | 是 | 合作伙伴名称,去首尾空格后不能为空 | +| `contactEmail` | string | 否 | 若填写则做简单格式校验(含 `@` 等) | +| `phoneNumber` | string | 否 | 电话 | +| `state` | bool | 否 | 默认 `true` | + +### 3.2 响应 + +- 成功:返回 `PartnerGetOutputDto`(与详情结构一致)。 + +--- + +## 4. 编辑 + +- **方法**:`PUT` +- **路径**:`/api/app/partner/{id}` + +### 4.1 参数 + +- **Path**:`id` 为当前合作伙伴主键。 +- **Body**:`PartnerUpdateInputVo`,字段与新增相同。 + +```json +{ + "partnerName": "Global Foods Inc.", + "contactEmail": "admin@globalfoods.com", + "phoneNumber": "+1 (555) 100-2000", + "state": false +} +``` + +### 4.2 响应 + +- 成功:返回更新后的 `PartnerGetOutputDto`。 + +--- + +## 5. 删除(逻辑删除) + +- **方法**:`DELETE` +- **路径**:`/api/app/partner/{id}` + +### 5.1 路径参数 + +| 参数 | 说明 | +|------|------| +| `id` | `fl_partner.Id` | + +### 5.2 行为 + +- 将 `IsDeleted` 置为 `true`,并更新 `LastModificationTime` / `LastModifierId`(若当前用户存在)。 + +--- + +## 6. 批量导出 PDF + +- **方法**:`GET` +- **路径**:`/api/app/partner/export-pdf` +- **响应**:`Content-Type: application/pdf`,附件名形如 `partners_yyyy-MM-dd_HH-mm-ss.pdf` + +### 6.1 查询参数 + +与列表接口相同的筛选字段(分页字段可忽略): + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `keyword` | string | 否 | 与列表 `keyword` 一致 | +| `state` | bool | 否 | 与列表 `state` 一致 | +| `sorting` | string | 否 | 与列表白名单一致,用于导出行顺序 | + +### 6.2 限制 + +- 命中行数 **超过 5000** 时接口返回业务错误,需缩小筛选范围后再导出。 +- 导出最多取 **5000** 条,排序与列表查询逻辑一致。 + +### 6.3 请求示例 + +```http +GET /api/app/partner/export-pdf?keyword=Global&state=true HTTP/1.1 +Authorization: Bearer {token} +``` + +### 6.4 PDF 内容说明 + +- 表头列:**Partner**、**Contact**、**Phone**、**Status**、**Created**。 +- `Status` 文本:`state === true` 时为 `active`,否则 `inactive`。 +- 空邮箱、空电话在 PDF 中显示为 **`无`**(与项目列表空值展示约定一致)。 + +--- + +## 7. 数据库与建表(Partner) + +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql` +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`PartnerName`、`ContactEmail`、`PhoneNumber`、`State` + +--- + +## 8. 与门店字段的关系说明 + +- 门店(`location`)上可能存在 **`Partner` 字符串字段**(原型/筛选用),与本文 **`fl_partner` 主数据表** 无强制外键关联。 +- 若后续要将门店关联到合作伙伴主数据,需单独产品方案(例如增加 `PartnerId` 或同步名称)。 + +--- + +## 9. 前端对接提示(Partner) + +- 列表「Search」对应 `keyword`;「Active」筛选对应 `state`。 +- 「Bulk Export (PDF)」调用 **第 6 节** 导出接口,查询参数与当前列表筛选保持一致即可。 + +--- + +# 第二部分:Group(组织 / Group) + +> UI:**Group Name**、**Parent Partner**(下拉绑定合作伙伴)、**Status**、**Bulk Export (PDF)**、**New+** 弹窗(Group Name、Assign to Partner、Active)。 + +## 10. 数据库与建表(Group) + +- 库中原先**无**独立 Group 业务表;新建表名:`fl_group`。 +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql` +- **须先存在 `fl_partner` 表**(脚本内含外键 `FK_fl_group_partner` → `fl_partner(Id)`)。 +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`GroupName`、`PartnerId`、`State` + +**建表 SQL(与脚本文件一致,便于直接执行):** + +```sql +CREATE TABLE IF NOT EXISTS `fl_group` ( + `Id` varchar(64) NOT NULL COMMENT '主键', + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', + `GroupName` varchar(256) NOT NULL COMMENT '组织名称', + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id', + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + PRIMARY KEY (`Id`), + KEY `IX_fl_group_IsDeleted` (`IsDeleted`), + KEY `IX_fl_group_State` (`State`), + KEY `IX_fl_group_PartnerId` (`PartnerId`), + KEY `IX_fl_group_GroupName` (`GroupName`(128)), + KEY `IX_fl_group_CreationTime` (`CreationTime`), + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)'; +``` + +--- + +## 11. Group — 分页列表 + +- **方法**:`GET` +- **路径**:`/api/app/group` + +### 11.1 查询参数(`GroupGetListInputVo`) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `skipCount` | int | 是 | 页码,从 **1** 开始 | +| `maxResultCount` | int | 是 | 每页条数 | +| `sorting` | string | 否 | 排序白名单(见下) | +| `keyword` | string | 否 | 模糊匹配 `GroupName`、所属 **未删除** Partner 的 `PartnerName` | +| `partnerId` | string | 否 | 仅查看某合作伙伴下的组织(`fl_partner.Id`) | +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 | + +**排序白名单**(大小写不敏感): + +- `GroupName asc` / `GroupName desc` +- `CreationTime asc` / `CreationTime desc` +- `State asc` / `State desc` +- `PartnerName asc` / `PartnerName desc`(按关联合作伙伴名称) + +其它值回退为默认:**按 `CreationTime` 降序**。 + +### 11.2 请求示例 + +```http +GET /api/app/group?skipCount=1&maxResultCount=10&keyword=West&partnerId={partnerGuid}&state=true HTTP/1.1 +Authorization: Bearer {token} +``` + +### 11.3 响应结构(`PagedResultWithPageDto`) + +```json +{ + "pageIndex": 1, + "pageSize": 10, + "totalCount": 2, + "totalPages": 1, + "items": [ + { + "id": "…", + "groupName": "West Coast Region", + "partnerId": "…", + "partnerName": "Global Foods Inc.", + "state": true, + "creationTime": "2026-04-27T10:00:00" + } + ] +} +``` + +### 11.4 列表项字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 主键 | +| `groupName` | string | 组织名称 | +| `partnerId` | string | 所属合作伙伴 Id | +| `partnerName` | string | 父级合作伙伴名称(UI「Parent Partner」) | +| `state` | bool | 是否启用 | +| `creationTime` | string (datetime) | 创建时间 | + +--- + +## 12. Group — 详情 + +- **方法**:`GET` +- **路径**:`/api/app/group/{id}` + +路径参数 `id`:`fl_group.Id`。响应为 `GroupGetOutputDto`(在列表字段基础上增加 `lastModificationTime`)。 + +--- + +## 13. Group — 新增 + +- **方法**:`POST` +- **路径**:`/api/app/group` + +### Body(`GroupCreateInputVo`) + +```json +{ + "groupName": "West Coast Region", + "partnerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "state": true +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `groupName` | string | 是 | 组织名称 | +| `partnerId` | string | 是 | Assign to Partner,须为未删除的 `fl_partner.Id` | +| `state` | bool | 否 | 默认 `true` | + +--- + +## 14. Group — 编辑 + +- **方法**:`PUT` +- **路径**:`/api/app/group/{id}` +- **Body**:`GroupUpdateInputVo`(字段同新增) + +--- + +## 15. Group — 删除(逻辑删除) + +- **方法**:`DELETE` +- **路径**:`/api/app/group/{id}` + +--- + +## 16. Group — 批量导出 PDF + +- **方法**:`GET` +- **路径**:`/api/app/group/export-pdf` +- **响应**:`application/pdf`,附件名形如 `groups_yyyy-MM-dd_HH-mm-ss.pdf` + +### 16.1 查询参数 + +与列表一致(分页可忽略):`keyword`、`partnerId`、`state`、`sorting`。 + +### 16.2 限制 + +- 命中行数 **超过 5000** 返回业务错误;导出最多 **5000** 条。 + +### 16.3 PDF 列 + +**Group Name**、**Parent Partner**、**Status**(`active` / `inactive`)、**Created**。 + +--- + +## 17. 前端对接提示(Group) + +- 「Search」→ `keyword`;按父级合作伙伴筛选 → `partnerId`(下拉选中项的 `id`);「Active」→ `state`。 +- 「Assign to Partner」下拉数据来自 **Partner 列表接口**(`/api/app/partner`)。 +- 「Bulk Export (PDF)」→ **第 16 节**,查询参数与当前列表筛选一致。 diff --git a/项目相关文档/平台端Categories图片上传接口说明.md b/项目相关文档/平台端Categories图片上传接口说明.md index edeb7d7..5742322 100644 --- a/项目相关文档/平台端Categories图片上传接口说明.md +++ b/项目相关文档/平台端Categories图片上传接口说明.md @@ -54,7 +54,7 @@ curl -X POST "http://localhost:19001/api/app/picture/category/upload" ^ | 字段 | 类型 | 说明 | |------|------|------| -| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) | +| `url` | string | 图片访问的相对路径;写入分类接口时,若 `categoryPhotoUrl` 采用 **JSON** 存展示数据,请将该 `url` 放入你方约定的 JSON 结构中。若仍传**纯路径字符串**,后端会序列化为 JSON 字符串再入库。 | | `fileName` | string | 服务器保存的文件名 | | `size` | number | 文件大小(字节) | @@ -96,7 +96,7 @@ curl -X POST "http://localhost:19001/api/app/picture/category/upload" ^ 推荐前端流程: 1. 调用本上传接口,拿到返回的 `url` -2. 再调用分类新增/编辑接口,把 `categoryPhotoUrl` 设置为该 `url` +2. 再调用分类新增/编辑接口:按平台与 **`buttonAppearance`(JSON 字符串)** 的约定组装 `categoryPhotoUrl`(JSON);或继续传纯 `url` 由后端自动包成 JSON 字符串。 -> 说明:分类 CRUD 已支持 `CategoryPhotoUrl` 字段;你只需要在页面表单里新增该字段即可。 +> 说明:详见 `项目相关文档/产品模块Categories接口对接说明.md`、`项目相关文档/标签模块接口对接说明.md` 中「JSON 字符串」约定。 diff --git a/项目相关文档/报表Reports接口对接说明.md b/项目相关文档/报表Reports接口对接说明.md new file mode 100644 index 0000000..7e55269 --- /dev/null +++ b/项目相关文档/报表Reports接口对接说明.md @@ -0,0 +1,112 @@ +# 报表 Reports 接口对接说明(Print Log / Label Report) + +> 适用范围:美国版 Web「Reports」— **Print Log** 列表与导出、**Label Report** 统计与导出、**重打** +> 实现:`IReportsAppService` / `ReportsAppService`;重打复用 `IUsAppLabelingAppService.ReprintAsync`(已支持 admin 跳过创建人校验) +> 路由前缀:`/api/app` + +--- + +## 0. 角色与数据范围(必读) + +- 判断依据:`CurrentUser.Roles` 中是否存在**忽略大小写**等于 **`admin`** 的角色码(与 JWT 中角色码一致,参见 `AuthSessionAppService` / `ReportsRoleHelper`)。 +- **`admin`**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 +- **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。 +- **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。 + +--- + +## 1. Partner / Group / Location 筛选说明 + +- **`locationId`**:若传则只查该门店(`location.Id` 字符串)。 +- **`partnerId`**(`fl_partner.Id`):按合作伙伴名称与 `location.Partner` **文本全等**(trim 后)匹配,得到门店集合再过滤任务。 +- **`groupId`**(`fl_group.Id`):按组织的 `GroupName` + 父级 `PartnerName` 与门店的 `GroupName` + `Partner` **文本全等**匹配门店。 +- 若 Partner/Group 在库中不存在,返回**空列表/空统计**(不报错)。 +- 未传 Partner/Group/Location 时:**不按门店集合预过滤**(仅日期、关键字、用户范围生效)。 + +--- + +## 2. Print Log — 分页查询 + +- **方法**:`GET` +- **路径**:`/api/app/reports/print-log-list`(以 Swagger 为准;ABP 约定多为 `get-print-log-list` 映射到该路径) + +### 2.1 查询参数(`ReportsPrintLogGetListInputVo`) + +| 参数 | 类型 | 说明 | +|------|------|------| +| `skipCount` | int | 页码,从 **1** 起 | +| `maxResultCount` | int | 每页条数 | +| `sorting` | string | 可选:`PrintedAt asc` / `PrintedAt desc`(默认倒序) | +| `partnerId` | string | 可选 | +| `groupId` | string | 可选 | +| `locationId` | string | 可选 | +| `startDate` | date | 可选;默认与结束日组成约 30 天窗口 | +| `endDate` | date | 可选;默认今天(含当日) | +| `keyword` | string | 可选;匹配产品名、标签分类名、产品分类名(模糊) | + +### 2.2 响应项(`ReportsPrintLogListItemDto`) + +含:`taskId`、`labelCode`、`productName`、`categoryName`、`templateText`、`printedAt`、`printedByName`、`locationText`、`locationId`(重打必填)、`expiryDateText`(从 `PrintInputJson` 中尝试解析 `expiryDate` / `expiry` / `expirationDate`)。 + +--- + +## 3. Print Log — 导出 PDF + +- **方法**:`GET` +- **路径**:`/api/app/reports/export-print-log-pdf` +- **查询参数**:与 **§2** 相同(分页字段忽略);最多 **5000** 条,超出返回业务错误。 + +--- + +## 4. Print Log — 重打(Reprint) + +- **方法**:`POST` +- **路径**:`/api/app/reports/reprint-print-log`(实现内转发至 `UsAppLabelingAppService.ReprintAsync`,以 Swagger 为准) +- **Body**:`UsAppLabelReprintInputVo`:`locationId`、`taskId`、`printQuantity`、`clientRequestId`(可选)、打印机字段(可选)。 + +--- + +## 5. Label Report — 统计聚合 + +- **方法**:`GET` +- **路径**:`/api/app/reports/label-report`(以 Swagger 为准) + +### 5.1 查询参数(`ReportsLabelReportQueryInputVo`) + +与 Print Log 相同的 `partnerId`、`groupId`、`locationId`、`startDate`、`endDate`、`keyword`(无分页)。 + +### 5.2 默认时间窗 + +未传日期时:**结束日 = 今天,开始日 = 结束日前推 29 天(共约 30 个自然日,含首尾)**。 + +### 5.3 返回(`ReportsLabelReportOutputDto`) + +- **`summary`**:`totalLabelsPrinted`、上一同长周期 `totalLabelsPrintedPrevPeriod`、`totalLabelsPrintedChangeRate`(%);最热门标签分类名与次数;Top 产品名与次数;`avgDailyPrints` 及环比等。 +- **`labelsByCategory`**:按 **标签分类**(`fl_label_category`)汇总当前区间内打印次数。 +- **`printVolumeTrend`**:在**当前筛选日期区间内**,取**结束日向前最多 7 个自然日**(与区间求交)的按日打印量。 +- **`mostUsedProducts`**:当前区间内产品打印次数 Top20,`usagePercent` 为占**当期总打印次数**的百分比。 + +--- + +## 6. Label Report — 导出 PDF + +- **方法**:`GET` +- **路径**:`/api/app/reports/export-label-report-pdf` +- **查询参数**:与 **§5** 相同;内容为摘要 + 分类表 + 日趋势表 + Top 产品表。 + +--- + +## 7. 数据表与字段依赖 + +- 打印任务:`fl_label_print_task`(`CreatedBy`、`LocationId`、`PrintedAt`、`PrintInputJson` 等) +- 门店:`location`(`Partner`、`GroupName`、`LocationCode`、`LocationName`) +- 主数据:`fl_partner`、`fl_group`(筛选用) +- 用户:`User`(展示 `PrintedByName`) + +--- + +## 8. 前端对接提示 + +- Print Log 行内 **Reprint**:使用列表返回的 `locationId` + `taskId` 调重打接口。 +- 「Export Report」在 Print Log Tab 调 **§3**;在 Label Report Tab 调 **§6**。 +- 下拉 **Partner / Group / Location** 与列表筛选字段一致;需保证门店维护的 `Partner` / `GroupName` 与主数据名称一致,否则筛选结果为空。 diff --git a/项目相关文档/本次新增与优化接口汇总.md b/项目相关文档/本次新增与优化接口汇总.md new file mode 100644 index 0000000..fece249 --- /dev/null +++ b/项目相关文档/本次新增与优化接口汇总.md @@ -0,0 +1,263 @@ +# 本次接口变更汇总(标签 / 产品类别 / 模板组件 / App 树 / Web 会话) + +> 说明:本文只汇总本次迭代中相关内容: +> - 标签模块 Label Categories:对齐“新增类别”原型图(`buttonAppearance` + `categoryPhotoUrl` / 展示文案 / 门店范围) +> - 产品模块 Categories:对齐“新增产品类别”原型图(同上) +> - 模板组件(`fl_label_template_element`):新增字段 `TypeAdd`(并补齐 `ElementName`) +> - App `labeling-tree`:L1 标签分类返回 `buttonAppearance` +> - Web 管理端 `auth-session`:**当前用户菜单与权限**、**退出登录**(食品标签-美国版模块) +> - Web `rbac-menu` 列表/详情:补充返回 `routerName`、`router` +> - 产品 **Products**:`POST/PUT /api/app/product` 支持可选 Body 字段 **`locationIds`**(多门店批量绑定 / 编辑时整表替换关联),详见 `项目相关文档/标签模块接口对接说明.md` **§6** +> +> 其余标签打印相关接口不在本文范围内。 + +--- + +## 1. 模板组件字段(`fl_label_template_element`) + +### 1.1 字段变更 + +- 新增字段:`TypeAdd`(元素附加类型,如 `label_Duration`) +- 新增字段:`ElementName`(元素名称,用于更稳定的显示/快照) + +### 1.2 接口影响范围 + +- 模板新增/编辑:保存 `elements[].typeAdd` / `elements[].elementName` +- 模板详情/预览:返回 `elements[].typeAdd` / `elements[].elementName` + +### 1.3 JSON 对齐(elements[]) + +| 前端字段 | 后端字段 | 说明 | +|---|---|---| +| `type` | `ElementType` | 元素类型 | +| `typeAdd` | `TypeAdd` | 元素附加类型 | +| `elementName` | `ElementName` | 元素名称 | + +--- + +## 2. 产品模块 Categories(Products → Categories) + +> 数据库侧你已完成:`fl_product_category` 新字段、`fl_product_category_location` 新表。 + +### 2.1 表结构要点 + +- `fl_product_category`(主表)关键字段: + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组) + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串 + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson` + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) +- `fl_product_category_location`(关联表): + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店 + +### 2.2 CRUD 接口(字段扩展) + +接口路径不变,仅扩展字段。 + +#### 2.2.1 列表 + +- **方法**:`GET` +- **路径**:`/api/app/product-category` +- **列表行新增返回**: + - `displayText` + - `buttonAppearance` + - `categoryPhotoUrl` + - `availabilityType` + +#### 2.2.2 详情 + +- **方法**:`GET` +- **路径**:`/api/app/product-category/{id}` +- **新增返回字段**: + - `displayText` + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析) + - `availabilityType` + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) + +#### 2.2.3 新增 + +- **方法**:`POST` +- **路径**:`/api/app/product-category` +- **新增入参字段**: + - `displayText` + - `buttonAppearance`、`categoryPhotoUrl` + - `availabilityType` + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个) + +#### 2.2.4 编辑 + +- **方法**:`PUT` +- **路径**:`/api/app/product-category/{id}` +- **入参同新增** + +#### 2.2.5 删除 + +- **方法**:`DELETE` +- **路径**:`/api/app/product-category/{id}` +- **说明**:逻辑删除;若被产品引用会阻止删除(保持原行为) + +### 2.3 后端校验规则(本次新增) + +- `availabilityType` 仅允许 `ALL/SPECIFIED` + - `SPECIFIED` 时 `locationIds` 至少 1 个 +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定 + +--- + +## 3. 标签模块 Label Categories(Labels → Label Categories) + +> 数据库侧新增:`fl_label_category` 新字段、`fl_label_category_location` 新表。 + +### 3.1 表结构要点 + +- `fl_label_category`(主表)关键字段: + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组) + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串 + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson` + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) +- `fl_label_category_location`(关联表): + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键) + +### 3.2 CRUD 接口(字段扩展) + +接口路径不变,仅扩展字段。 + +#### 3.2.1 列表 + +- **方法**:`GET` +- **路径**:`/api/app/label-category` +- **列表行新增返回**: + - `displayText` + - `buttonAppearance` + - `categoryPhotoUrl` + - `availabilityType` + +#### 3.2.2 详情 + +- **方法**:`GET` +- **路径**:`/api/app/label-category/{id}` +- **新增返回字段**: + - `displayText` + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析) + - `availabilityType` + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) + +#### 3.2.3 新增 + +- **方法**:`POST` +- **路径**:`/api/app/label-category` +- **新增入参字段**: + - `displayText` + - `buttonAppearance`、`categoryPhotoUrl` + - `availabilityType` + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个) + +#### 3.2.4 编辑 + +- **方法**:`PUT` +- **路径**:`/api/app/label-category/{id}` +- **入参同新增** + +#### 3.2.5 删除 + +- **方法**:`DELETE` +- **路径**:`/api/app/label-category/{id}` +- **说明**:逻辑删除;若被标签引用会阻止删除(保持原行为) + +### 3.3 后端校验规则(本次新增) + +- `availabilityType` 仅允许 `ALL/SPECIFIED` + - `SPECIFIED` 时 `locationIds` 至少 1 个 +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定 + +--- + +## 4. App 端 `GET /api/app/us-app-labeling/labeling-tree` + +- **L1(标签分类)节点**:返回 `categoryPhotoUrl`、`buttonAppearance`(均为库中字符串,**多为 JSON**,与 CRUD 一致);缺省或空时 `buttonAppearance` 后端默认 **`"TEXT"`**(兼容旧数据,**不再**对整段做 `ToUpperInvariant` 以免破坏 JSON)。 +- **L2(产品分类)节点**:返回 `displayText`、`buttonAppearance`、`categoryPhotoUrl`、`availabilityType`、`orderNum` 等;外观数据由 **`buttonAppearance` + `categoryPhotoUrl`** 承载(已不再返回 `buttonTextColor`、`buttonBgColor`、`buttonImageUrl`、`buttonStyleJson`)。 + +### 4.1 数据库迁移(两张主表) + +在确认历史数据已按需迁到 `CategoryPhotoUrl` 后,可执行(列不存在时需跳过或调整): + +```sql +ALTER TABLE `fl_label_category` + DROP COLUMN `ButtonTextColor`, + DROP COLUMN `ButtonBgColor`, + DROP COLUMN `ButtonImageUrl`, + DROP COLUMN `ButtonStyleJson`; + +ALTER TABLE `fl_product_category` + DROP COLUMN `ButtonTextColor`, + DROP COLUMN `ButtonBgColor`, + DROP COLUMN `ButtonImageUrl`, + DROP COLUMN `ButtonStyleJson`; +``` + +--- + +## 5. Web 管理端会话(`AuthSession` / 食品标签-美国版) + +> 实现:`IAuthSessionAppService` / `AuthSessionAppService`。需携带与后台一致的 **JWT**(`Authorization: Bearer {token}`)。具体 action 路径以部署环境 **Swagger / OpenAPI** 为准;下列为 ABP 常规约定(`RootPath = api/app`)。 + +### 5.1 获取当前登录用户菜单与权限 + +- **方法**:`GET` +- **路径**(约定):`/api/app/auth-session/my-menus` +- **鉴权**:需要登录 +- **用途**:前端动态路由、侧边栏、按钮级权限(`permissionCodes`) +- **返回体**(`CurrentUserMenuPermissionsOutputDto`,JSON 字段名为 camelCase): + +| 字段 | 类型 | 说明 | +|---|---|---| +| `user` | object | 当前用户简要信息(无密码) | +| `user.id` | guid | 用户 Id | +| `user.userName` | string | 登录名 | +| `user.nick` | string? | 昵称 | +| `user.email` | string? | 邮箱 | +| `user.icon` | string? | 头像 | +| `roleCodes` | string[] | 角色编码列表(已排序) | +| `permissionCodes` | string[] | 权限码列表(已排序;超级管理员常见为 `*:*:*`) | +| `menus` | array | **菜单树**(根节点列表,子节点在 `children`) | + +**菜单树节点**(`CurrentUserMenuNodeDto`)主要字段: + +| 字段 | 说明 | +|---|---| +| `id` / `parentId` | 菜单 Id、父 Id(根父级多为 `"0"`) | +| `menuName` | 菜单名称 | +| `routerName` / `router` | 路由名、路径 | +| `permissionCode` | 权限标识(按钮/接口控制用) | +| `menuType` / `menuSource` | 枚举整型值(与 `Menu` 表一致) | +| `orderNum` / `state` | 排序、是否启用 | +| `menuIcon` / `component` / `isLink` / `isCache` / `isShow` / `query` / `remark` | 与菜单表一致 | +| `children` | 子节点数组 | + +**业务说明**: + +- 数据来源与框架 `UserManager.GetInfoAsync` 一致:按用户角色合并菜单与权限码。 +- 用户名为 **`admin`** 时:与 `AccountService.GetVue3Router` 对齐,返回 **`Menu` 表中未逻辑删除** 的全部菜单再组树(`permissionCodes` 仍为超级管理员约定值)。 + +### 5.2 退出登录 + +- **方法**:`POST` +- **路径**(约定):`/api/app/auth-session/logout` +- **鉴权**:需要登录(未登录或无法解析用户时返回 `false`) +- **请求体**:无 +- **返回**:`boolean` + - `true`:已清除服务端 **用户信息分布式缓存**(与 `AccountService.PostLogout` 一致) + - `false`:当前请求未识别到用户 Id(例如未登录) +- **说明**:JWT 为无状态令牌,**前端仍需丢弃本地 Token**;退出接口主要清理服务端缓存侧用户信息。 + +--- + +## 6. 权限菜单 `rbac-menu` 列表/详情补充字段 + +- **路径**:`GET /api/app/rbac-menu`(列表)、`GET /api/app/rbac-menu/{id}`(详情) +- **新增返回**(与 `menu` 表字段一致,JSON 一般为 camelCase): + - `routerName`:路由名称 + - `router`:路由路径 +- **说明**:树接口 `GET /api/app/rbac-menu/tree`(若已使用)本身已包含完整菜单字段,无需重复改动。 + diff --git a/项目相关文档/标签模块接口对接说明.md b/项目相关文档/标签模块接口对接说明.md index d21d1dc..6cb7c8d 100644 --- a/项目相关文档/标签模块接口对接说明.md +++ b/项目相关文档/标签模块接口对接说明.md @@ -51,6 +51,12 @@ Swagger 地址: } ``` +### 1.1.1 字段约定:`buttonAppearance` 与 `categoryPhotoUrl`(JSON 字符串) + +- **`buttonAppearance`**:库中存 **JSON 文本**(如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]` 等);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 `["TEXT"]` 等)。未传或空白时后端默认 `["TEXT"]`。非法值(非 JSON 且非上述三者)会返回友好错误。 +- **`categoryPhotoUrl`**:同样为 **JSON 文本**(如 `["Prep","#10B981"]`);若传**非 JSON** 的纯文本(色值、`/picture/...` 等),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**字符串,由客户端解析。 +- 其它常用字段:`displayText`、`availabilityType`(`ALL`/`SPECIFIED`)、`locationIds`(指定门店时必填),与产品类别接口语义一致(见 `项目相关文档/产品模块Categories接口对接说明.md`)。 + ### 1.2 详情 方法:`GET /api/app/label-category/{id}` @@ -69,7 +75,11 @@ Swagger 地址: { "categoryCode": "CAT_PREP", "categoryName": "Prep", - "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png", + "displayText": "Prep", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "availabilityType": "ALL", + "locationIds": [], "state": true, "orderNum": 1 } @@ -85,7 +95,11 @@ Swagger 地址: { "categoryCode": "CAT_PREP", "categoryName": "Prep", - "categoryPhotoUrl": null, + "displayText": "Prep", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "availabilityType": "ALL", + "locationIds": [], "state": true, "orderNum": 2 } @@ -560,6 +574,10 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI 入参: - `id`:产品Id(`fl_product.Id`) +返回(`ProductGetOutputDto`,与实现一致的主要字段): +- `id`、`productCode`、`productName`、`categoryId`、`categoryName`、`productImageUrl`、`state` +- **`locationIds`**:`string[]`,该产品在 **`fl_location_product`** 中绑定的门店 Id(去重);无关联时为空数组 + ### 6.3 新增产品 方法:`POST /api/app/product` @@ -569,15 +587,30 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI { "productCode": "PRD_TEST_001", "productName": "Chicken", - "categoryName": "Meat", + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", "productImageUrl": "https://example.com/img.png", - "state": true + "state": true, + "locationIds": [ + "11111111-1111-1111-1111-111111111111", + "22222222-2222-2222-2222-222222222222" + ] } ``` +字段说明: +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `productCode` | string | 是 | 产品编码 | +| `productName` | string | 是 | 产品名称 | +| `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | +| `productImageUrl` | string \| null | 否 | 主图 URL | +| `state` | bool | 否 | 默认 `true` | +| **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | + 校验: -- `productCode/productName` 不能为空 +- `productCode` / `productName` 不能为空 - `productCode` 不能与未删除的数据重复 +- 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) ### 6.4 编辑产品 @@ -585,7 +618,13 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI 入参: - Path:`id` 为当前产品Id(`fl_product.Id`) -- Body:字段同新增(`ProductUpdateInputVo`) +- Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) + +**`locationIds` 行为(与新增不同,请注意):** +- **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 +- **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 + +其它校验同 **§6.3**(含门店存在性校验,当 `locationIds` 含非空项时)。 ### 6.5 删除(逻辑删除) @@ -599,6 +638,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI 说明: - 关联表:`fl_location_product` +- 也可在 **§6.3 / §6.4** 通过产品 Body 的 **`locationIds`** 一次性维护本产品在各门店的关联(与 §7 写入同一张表);二者可并存,按需选择调用方式。 - 关联按门店进行批量替换: - `Create`:在门店下新增未存在的 product 关联 - `Update`:替换该门店下全部关联(先删后建) @@ -706,7 +746,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI |------|------|------| | `id` | string | `fl_label_category.Id` | | `categoryName` | string | 分类名称 | -| `categoryPhotoUrl` | string \| null | 分类图标/图 | +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) | +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) | | `orderNum` | number | 排序 | | `productCategories` | array | 第二级列表(见下表) | @@ -715,8 +756,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | 字段 | 类型 | 说明 | |------|------|------| | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 | | `name` | string | 产品分类显示名;空源数据为 **`无`** | +| `displayText` | string \| null | 按钮展示文案 | +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | +| `orderNum` | number | 排序 | | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | | `products` | array | 第三级产品列表(见下表) | @@ -771,13 +816,18 @@ curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati { "id": "cat-prep-id", "categoryName": "Prep", - "categoryPhotoUrl": "/picture/...", + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]", + "buttonAppearance": "[\"TEXT\",\"COLOR\"]", "orderNum": 1, "productCategories": [ { "categoryId": "pc-meat-id", - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]", "name": "Meat", + "displayText": "Meat", + "buttonAppearance": "[\"IMAGE\"]", + "availabilityType": "ALL", + "orderNum": 10, "itemCount": 1, "products": [ { diff --git a/项目相关文档/美国版App登录接口说明.md b/项目相关文档/美国版App登录接口说明.md index fd04601..6859c10 100644 --- a/项目相关文档/美国版App登录接口说明.md +++ b/项目相关文档/美国版App登录接口说明.md @@ -149,11 +149,294 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... --- +## 接口 3:我的资料(My Profile) + +用于「My Profile」页展示:全名、邮箱、电话、员工号、角色。 + +### HTTP + +- **方法**:`GET` +- **路径**:`/api/app/us-app-auth/my-profile`(以 Swagger 中 `UsAppAuth` 为准) +- **鉴权**:需要登录(`Authorization: Bearer {token}`) + +### 请求参数 + +无。 + +### 响应体(UsAppMyProfileOutputDto) + +| 字段(JSON) | 类型 | 说明 | +|--------------|------|------| +| `fullName` | string | `User.Name`,无则 `Nick`,再无则 `UserName` | +| `email` | string | `User.Email`;空为 `"无"` | +| `phone` | string | 由 `User.Phone`(long)格式化为可读字符串;10 位数字按 `+1 (555) 123-4567` 形式;无则 `"无"` | +| `employeeId` | string | 当前取 **`User.UserName`**(可与业务约定为工号/登录名) | +| `roleDisplay` | string | 多角色 `Role.RoleName` 按 `OrderNum` 排序后以英文逗号拼接;无角色为 `"无"` | +| `primaryRoleCode` | string \| null | 第一个角色的 `RoleCode`,供前端样式 | + +--- + +## 接口 4:修改密码(Change Password) + +与 **`User.JudgePassword` / `BuildPassword`** 一致:校验当前密码后写入新盐值哈希。 + +### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/us-app-auth/change-password` +- **Content-Type**:`application/json` +- **鉴权**:需要登录 + +### 请求体(UsAppChangePasswordInputVo) + +| 字段(JSON) | 类型 | 必填 | 说明 | +|--------------|------|------|------| +| `currentPassword` | string | 是 | 当前明文密码 | +| `newPassword` | string | 是 | 新密码 | +| `confirmNewPassword` | string | 是 | 必须与 `newPassword` **完全一致** | + +### 新密码复杂度(与原型「Password Requirements」一致) + +1. 至少 **8** 位 +2. 至少 **1** 个大写字母、**1** 个小写字母 +3. 至少 **1** 个数字 +4. 至少 **1** 个**非字母数字**字符(特殊字符) + +### 请求示例 + +```json +{ + "currentPassword": "OldPass1!", + "newPassword": "NewPass9@x", + "confirmNewPassword": "NewPass9@x" +} +``` + +### 响应 + +成功时 HTTP 200,无业务体要求(ABP 可能返回空对象);失败为业务异常文案。 + +### 常见错误 + +- 未登录:`用户未登录` +- 当前密码错误:与登录失败一致(`UserConst.Login_Error` 文案) +- 新密码与确认不一致:`新密码与确认密码不一致` +- 新密码与当前相同:`新密码不能与当前密码相同` +- 不满足复杂度:如 `新密码至少 8 位`、`新密码需包含大写字母` 等 + +--- + +## 接口 5:门店详情(Location) + +用于移动端「Location」页:店名、完整地址、门店电话、营业时间占位、店长姓名与电话。**仅当当前 JWT 用户在 `userlocation` 中绑定该门店**时可查(与接口 1、2 的门店范围一致)。 + +### HTTP + +- **方法**:`GET` +- **路径**:`/api/app/us-app-auth/location-detail/{locationId}`(以 Swagger 中 `UsAppAuth` → `GetLocationDetail` 为准) +- **鉴权**:需要登录(`Authorization: Bearer {token}`) + +### 请求参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| `locationId` | Path | string | 是 | 门店主键,Guid 字符串(与 `locations[].id` 一致) | + +### 传参示例 + +```http +GET /api/app/us-app-auth/location-detail/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1 +Host: localhost:19001 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +### 响应体(UsAppLocationDetailOutputDto) + +| 字段(JSON) | 类型 | 说明 | +|--------------|------|------| +| `locationId` | string | 门店主键 | +| `locationName` | string | 门店名称;空为 `"无"` | +| `fullAddress` | string | 与登录接口相同的地址拼接规则;无有效片段为 `"无"` | +| `storePhone` | string | `location.Phone` 去首尾空格;空为 `"无"` | +| `operatingHours` | string | 当前 **`location` 表无营业时间字段**,固定返回 **`"无"`**(后续若落库可再对接) | +| `managerName` | string | 在同店 `userlocation` 绑定用户中,取角色 `RoleCode` / `RoleName`(忽略大小写)包含 **`manager`** 的第一人展示姓名(`Name` → `Nick` → `UserName`);无匹配为 **`"无"`** | +| `managerPhone` | string | 同上用户的 `User.Phone`(long)按与「我的资料」相同的可读格式;无电话为 **`"无"`** | + +### 响应示例 + +```json +{ + "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", + "locationName": "Downtown Store", + "fullAddress": "123 Main St, New York, NY 10001", + "storePhone": "(212) 555-0100", + "operatingHours": "无", + "managerName": "Jane Doe", + "managerPhone": "+1 (555) 123-4567" +} +``` + +### 常见错误(业务异常文案) + +- 未登录:`用户未登录` +- `locationId` 非法或空:`无效的门店标识` +- 当前用户未绑定该门店:`当前账号未绑定该门店,无法查看` +- 门店已删或不存在:`门店不存在或已删除` + +--- + +## 接口 6:全局 Support 联系方式(App 只读 + Web 可读) + +用于 App「Support」页与 Web 展示**全平台共用**的一条电话与邮箱。 +实现:`LocationSupportAppService`,表 `fl_location_support` **不再包含门店 Id**。 + +### HTTP(查询) + +- **方法**:`GET` +- **路径**(约定式 API,以 Swagger 中 `LocationSupport` → `GetSupport` 为准):一般为 **`/api/app/location-support/support`** +- **鉴权**:需要登录(`Authorization: Bearer {token}`)。**App 登录 Token 与 Web Token 均可调用本接口。** + +### 请求参数 + +无 Query / Body。 + +### 响应体(LocationSupportGetOutputDto) + +| 字段(JSON) | 类型 | 说明 | +|--------------|------|------| +| `id` | string | 记录主键(Web 编辑 `PUT` 路径中的 `{id}`) | +| `supportPhone` | string | Support 电话 | +| `supportEmail` | string | Support 邮箱 | + +> 若尚未在后台配置,接口返回 `null`。 + +### 响应示例 + +```json +{ + "id": "3a2f4fda-1a93-4a35-9b98-95dca7bb5d2a", + "supportPhone": "1-800-SUPPORT", + "supportEmail": "support@medvantage.com" +} +``` + +### App 与 Web 权限说明 + +- App 登录签发 JWT 时会写入声明 **`client_kind` = `us-app`**(与 Web 管理端 Token 区分)。 +- **App 仅允许调用本节的 `GET`(查询)**;若使用 App Token 调用新增/编辑,将返回业务错误(英文):`The mobile app can only view support contacts. Please use the web console to edit.` + +--- + +## 后台维护接口:Location Support(Web:新增 / 编辑) + +仅 **Web 管理端 Token**(无 `client_kind=us-app`)可调用,用于维护全局 Support 联系方式。 + +### 接口 A:新增(全局一条) + +- **方法**:`POST` +- **路径**:`/api/app/location-support` +- **Content-Type**:`application/json` + +请求体(LocationSupportCreateInputVo): + +```json +{ + "supportPhone": "1-800-SUPPORT", + "supportEmail": "support@medvantage.com" +} +``` + +约束: + +- 系统内仅允许存在一条未删除记录;若已存在,再次新增会报错:`Global support contact already exists. Use update instead.` + +### 接口 B:编辑 + +- **方法**:`PUT` +- **路径**:`/api/app/location-support/{id}` +- **Content-Type**:`application/json` + +请求体(LocationSupportUpdateInputVo): + +```json +{ + "supportPhone": "1-800-SUPPORT", + "supportEmail": "support@medvantage.com" +} +``` + +常见错误(英文): + +- `The mobile app can only view support contacts. Please use the web console to edit.` +- `Support phone is required.` / `Support email is required.` / `Support email format is invalid.` +- `Global support contact already exists. Use update instead.` +- `Support record not found.` / `Support record id is required.` + +### 数据库迁移(删除 `LocationId`) + +若线上表仍为旧结构(含 `LocationId`),请在库中执行脚本: + +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql` + +新建库请使用: + +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql` + +--- + +## 平台端登录接口(Web) + +> 对应 `RBAC.AccountService.PostLoginAsync`。当前平台端登录规则已调整为:**仅支持 Email 作为账号,不再支持 UserName**。 + +### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/account/login`(以 Swagger 中 `Account` 为准) +- **Content-Type**:`application/json` +- **鉴权**:匿名 + +### 请求体(LoginInputVo) + +| 字段(JSON) | 类型 | 必填 | 说明 | +|--------------|------|------|------| +| `userName` | string | 是 | 兼容历史字段名;现必须传 **邮箱**(例如 `admin@example.com`) | +| `password` | string | 是 | 密码 | +| `uuid` | string | 条件 | 开启图形验证码时必填 | +| `code` | string | 条件 | 开启图形验证码时必填 | + +### 请求示例 + +```json +{ + "userName": "admin@example.com", + "password": "YourPassword", + "uuid": "captcha-uuid", + "code": "captcha-code" +} +``` + +### 响应体(LoginOutputDto) + +| 字段(JSON) | 类型 | 说明 | +|--------------|------|------| +| `token` | string | Access Token | +| `refreshToken` | string | Refresh Token | + +### 错误文案(英文) + +- `Email and password are required.` +- `Sign-in failed: email is required as the account.` +- `Sign-in failed: account not found.` +- `Sign-in failed: incorrect email or password.` +- `Invalid captcha.` + +--- + ## 与其他登录方式的区别 | 场景 | 说明 | |------|------| -| Web 管理端 | 仍使用 RBAC **`AccountService.PostLoginAsync`**,一般为人 **`userName`** + 密码 | -| 美国版 App | **仅**本模块 **`/api/app/us-app-auth/login`** 使用 **邮箱 + 密码** | +| Web 管理端 | 使用 RBAC **`/api/app/account/login`**,字段名虽为 `userName`,但值必须是 **Email** | +| 美国版 App | 使用 **`/api/app/us-app-auth/login`**,显式字段 `email` + `password` | 两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。