From 4d328ec29539558a2d3c3a161635276a63c9fc83 Mon Sep 17 00:00:00 2001 From: 李曜臣 Date: Mon, 27 Apr 2026 19:54:41 +0800 Subject: [PATCH] 平台端报表reports,仪表盘Dashboard统计缺失项补齐,合作伙伴partner; app我的资料,门店信息 --- 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardOverviewOutputDto.cs | 3 +++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs | 31 +++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs | 16 ++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs | 24 ++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs | 22 ++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs | 21 +++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs | 13 +++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs | 15 +++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs | 19 +++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs | 19 +++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs | 21 +++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs | 15 +++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs | 19 +++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs | 21 +++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs | 35 +++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs | 16 ++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs | 28 ++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs | 25 +++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs | 2 +- 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs | 38 ++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs | 31 +++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj | 1 + 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs | 31 +++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs | 38 ++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs | 43 +++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs | 356 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs | 326 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs | 777 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs | 263 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs | 5 +++-- 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql | 21 +++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql | 20 ++++++++++++++++++++ 项目相关文档/Dashboard统计接口对接说明.md | 33 +++++++++++++++++++++++++++++++++ 项目相关文档/合作伙伴Partner接口对接说明.md | 417 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 项目相关文档/报表Reports接口对接说明.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 项目相关文档/美国版App登录接口说明.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 40 files changed, 3369 insertions(+), 3 deletions(-) create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql create mode 100644 项目相关文档/合作伙伴Partner接口对接说明.md create mode 100644 项目相关文档/报表Reports接口对接说明.md 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 index 03bc5fc..195477e 100644 --- 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 @@ -41,6 +41,9 @@ public class DashboardOverviewOutputDto /// 按分类分布总数(前端直观命名) 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/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/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..aacfd87 --- /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; } = string.Empty; + + 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; } = string.Empty; + + 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/IServices/IDashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs index f14fb75..4e825da 100644 --- 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 @@ -9,7 +9,7 @@ namespace FoodLabeling.Application.Contracts.IServices; public interface IDashboardAppService : IApplicationService { /// - /// 获取 Dashboard 总览统计(卡片 + 周趋势 + 分类分布) + /// 获取 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/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/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/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/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/DashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs index 287be59..94470a5 100644 --- 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 @@ -1,7 +1,10 @@ +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; @@ -127,6 +130,72 @@ public class DashboardAppService : ApplicationService, IDashboardAppService .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); @@ -156,12 +225,100 @@ public class DashboardAppService : ApplicationService, IDashboardAppService 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; 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/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/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/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/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..1218b35 --- /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 = x.Id ?? string.Empty, + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? "无" : 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 = x.Id ?? string.Empty, + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? "无" : x.CategoryName!.Trim(), + TotalPrinted = x.Cnt, + UsagePercent = pct + }; + }).ToList(); + + return new ReportsLabelReportOutputDto + { + Summary = new ReportsLabelReportSummaryDto + { + TotalLabelsPrinted = totalCur, + TotalLabelsPrintedPrevPeriod = totalPrev, + TotalLabelsPrintedChangeRate = CalcChangeRate(totalCur, totalPrev), + MostPrintedCategoryName = topCat is null ? "无" : (topCat.CategoryName?.Trim() ?? "无"), + MostPrintedCategoryCount = topCat?.Cnt ?? 0, + TopProductName = topProd is 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..44ebf48 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,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; @@ -20,6 +21,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 +137,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("用户不存在或已停用"); + } + + var roleRows = await _dbContext.SqlSugarClient.Queryable((ur, r) => ur.RoleId == r.Id) + .Where((ur, r) => ur.UserId == userId && !r.IsDeleted && r.State) + .OrderBy((ur, r) => r.OrderNum) + .Select((ur, r) => new { r.RoleName, r.RoleCode }) + .ToListAsync(); + + 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.Queryable( + (u, ur, r) => u.Id == ur.UserId && ur.RoleId == r.Id) + .Where((u, ur, r) => userGuids.Contains(u.Id) && !u.IsDeleted && u.State) + .Where((u, ur, r) => !r.IsDeleted && r.State) + .Where((u, ur, r) => + SqlFunc.ToLower(r.RoleCode).Contains("manager") || + SqlFunc.ToLower(r.RoleName).Contains("manager")) + .OrderBy((u, ur, r) => u.Name) + .Select((u, ur, r) => new + { + u.Name, + u.Nick, + u.UserName, + u.Phone + }) + .ToListAsync(); + + 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) 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 bb294a3..7729567 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 @@ -613,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("无权限重打该任务"); } 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_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/项目相关文档/Dashboard统计接口对接说明.md b/项目相关文档/Dashboard统计接口对接说明.md index e62e28b..af28dbc 100644 --- a/项目相关文档/Dashboard统计接口对接说明.md +++ b/项目相关文档/Dashboard统计接口对接说明.md @@ -28,6 +28,7 @@ "weeklyPrintVolume": [], "byCategory": [], "byCategoryTotal": 0, + "recentLabels": [], "generatedAt": "2026-04-22T10:00:00+08:00", "metricCards": [], @@ -39,6 +40,7 @@ 说明: - `labelsPrintedToday/activeTemplates/...`、`byCategory/byCategoryTotal` 是**前端直观命名**(推荐使用)。 - `metricCards`、`categoryDistribution`、`categoryDistributionTotal` 为**兼容字段**(与旧版返回一致)。 +- `recentLabels`:**Recent Labels** 区块数据,全门店按打印时间倒序取最新 **10** 条(`fl_label_print_task`)。 --- @@ -88,6 +90,25 @@ --- +### 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 @@ -155,6 +176,18 @@ { "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": [], 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/项目相关文档/报表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/项目相关文档/美国版App登录接口说明.md b/项目相关文档/美国版App登录接口说明.md index fd04601..9da9fa1 100644 --- a/项目相关文档/美国版App登录接口说明.md +++ b/项目相关文档/美国版App登录接口说明.md @@ -149,6 +149,142 @@ 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`(以 Swagger 中 `UsAppAuth` → `GetLocationDetail` 为准;`locationId` 一般为 **Query** 参数) +- **鉴权**:需要登录(`Authorization: Bearer {token}`) + +### 请求参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| `locationId` | Query | string | 是 | 门店主键,Guid 字符串(与 `locations[].id` 一致) | + +### 传参示例 + +```http +GET /api/app/us-app-auth/location-detail?locationId=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` 非法或空:`无效的门店标识` +- 当前用户未绑定该门店:`当前账号未绑定该门店,无法查看` +- 门店已删或不存在:`门店不存在或已删除` + +--- + ## 与其他登录方式的区别 | 场景 | 说明 | -- libgit2 0.21.4