Commit 4d328ec29539558a2d3c3a161635276a63c9fc83
1 parent
3b307558
平台端报表reports,仪表盘Dashboard统计缺失项补齐,合作伙伴partner;
app我的资料,门店信息
Showing
40 changed files
with
3369 additions
and
3 deletions
美国版/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 |
| 41 | 41 | /// <summary>按分类分布总数(前端直观命名)</summary> |
| 42 | 42 | public int ByCategoryTotal { get; set; } |
| 43 | 43 | |
| 44 | + /// <summary>最近打印标签(全门店最新若干条,用于 Recent Labels 区块)</summary> | |
| 45 | + public List<DashboardRecentLabelItemDto> RecentLabels { get; set; } = new(); | |
| 46 | + | |
| 44 | 47 | /// <summary>统计时间</summary> |
| 45 | 48 | public DateTime GeneratedAt { get; set; } |
| 46 | 49 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Dashboard「Recent Labels」单行数据(最近打印记录) | |
| 5 | +/// </summary> | |
| 6 | +public class DashboardRecentLabelItemDto | |
| 7 | +{ | |
| 8 | + /// <summary>打印任务 Id(fl_label_print_task.Id)</summary> | |
| 9 | + public string TaskId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>标签编码(界面 Serial / Label ID,如 1-251201)</summary> | |
| 12 | + public string LabelCode { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>展示名称:优先产品名,否则标签名称</summary> | |
| 15 | + public string DisplayName { get; set; } = "无"; | |
| 16 | + | |
| 17 | + /// <summary>打印人用户 Id(CreatedBy)</summary> | |
| 18 | + public string? PrintedByUserId { get; set; } | |
| 19 | + | |
| 20 | + /// <summary>打印人展示名(User.Name 或 UserName)</summary> | |
| 21 | + public string PrintedByName { get; set; } = "无"; | |
| 22 | + | |
| 23 | + /// <summary>打印时间(PrintedAt 优先,否则 CreationTime)</summary> | |
| 24 | + public DateTime PrintedAt { get; set; } | |
| 25 | + | |
| 26 | + /// <summary>状态:<c>active</c> 或 <c>expired</c>(依据 PrintInputJson 中保质期与当前日期比较)</summary> | |
| 27 | + public string Status { get; set; } = "active"; | |
| 28 | + | |
| 29 | + /// <summary>角标/尺寸短文案(如 2"x2",用于左侧圆标)</summary> | |
| 30 | + public string LabelTypeBadge { get; set; } = "无"; | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 新建组织入参 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupCreateInputVo | |
| 7 | +{ | |
| 8 | + public string GroupName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + /// <summary> | |
| 11 | + /// 指派到的合作伙伴 Id(Assign to Partner) | |
| 12 | + /// </summary> | |
| 13 | + public string PartnerId { get; set; } = string.Empty; | |
| 14 | + | |
| 15 | + public bool State { get; set; } = true; | |
| 16 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 组织(Group)分页查询入参 | |
| 7 | +/// </summary> | |
| 8 | +public class GroupGetListInputVo : PagedAndSortedResultRequestDto | |
| 9 | +{ | |
| 10 | + /// <summary> | |
| 11 | + /// 模糊搜索(GroupName、所属 Partner 的 PartnerName) | |
| 12 | + /// </summary> | |
| 13 | + public string? Keyword { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 按所属合作伙伴筛选(fl_partner.Id) | |
| 17 | + /// </summary> | |
| 18 | + public string? PartnerId { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 启用状态 | |
| 22 | + /// </summary> | |
| 23 | + public bool? State { get; set; } | |
| 24 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 组织列表项 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupGetListOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string GroupName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string PartnerId { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary> | |
| 15 | + /// 所属合作伙伴名称(列表「Parent Partner」列) | |
| 16 | + /// </summary> | |
| 17 | + public string PartnerName { get; set; } = string.Empty; | |
| 18 | + | |
| 19 | + public bool State { get; set; } | |
| 20 | + | |
| 21 | + public DateTime CreationTime { get; set; } | |
| 22 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 组织详情 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupGetOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string GroupName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string PartnerId { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string PartnerName { get; set; } = string.Empty; | |
| 15 | + | |
| 16 | + public bool State { get; set; } | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | + | |
| 20 | + public DateTime? LastModificationTime { get; set; } | |
| 21 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 编辑组织入参 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupUpdateInputVo | |
| 7 | +{ | |
| 8 | + public string GroupName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string PartnerId { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public bool State { get; set; } = true; | |
| 13 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 新建合作伙伴入参 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerCreateInputVo | |
| 7 | +{ | |
| 8 | + public string PartnerName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string? ContactEmail { get; set; } | |
| 11 | + | |
| 12 | + public string? PhoneNumber { get; set; } | |
| 13 | + | |
| 14 | + public bool State { get; set; } = true; | |
| 15 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 合作伙伴分页查询入参 | |
| 7 | +/// </summary> | |
| 8 | +public class PartnerGetListInputVo : PagedAndSortedResultRequestDto | |
| 9 | +{ | |
| 10 | + /// <summary> | |
| 11 | + /// 模糊搜索(PartnerName / ContactEmail / PhoneNumber) | |
| 12 | + /// </summary> | |
| 13 | + public string? Keyword { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 启用状态(与列表筛选一致) | |
| 17 | + /// </summary> | |
| 18 | + public bool? State { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 合作伙伴列表项 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerGetListOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string PartnerName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string? ContactEmail { get; set; } | |
| 13 | + | |
| 14 | + public string? PhoneNumber { get; set; } | |
| 15 | + | |
| 16 | + public bool State { get; set; } | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 合作伙伴详情 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerGetOutputDto | |
| 7 | +{ | |
| 8 | + public string Id { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string PartnerName { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + public string? ContactEmail { get; set; } | |
| 13 | + | |
| 14 | + public string? PhoneNumber { get; set; } | |
| 15 | + | |
| 16 | + public bool State { get; set; } | |
| 17 | + | |
| 18 | + public DateTime CreationTime { get; set; } | |
| 19 | + | |
| 20 | + public DateTime? LastModificationTime { get; set; } | |
| 21 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 编辑合作伙伴入参 | |
| 5 | +/// </summary> | |
| 6 | +public class PartnerUpdateInputVo | |
| 7 | +{ | |
| 8 | + public string PartnerName { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string? ContactEmail { get; set; } | |
| 11 | + | |
| 12 | + public string? PhoneNumber { get; set; } | |
| 13 | + | |
| 14 | + public bool State { get; set; } = true; | |
| 15 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Label Report 聚合结果(卡片 + 图表 + Top 产品表) | |
| 5 | +/// </summary> | |
| 6 | +public class ReportsLabelReportOutputDto | |
| 7 | +{ | |
| 8 | + public ReportsLabelReportSummaryDto Summary { get; set; } = new(); | |
| 9 | + | |
| 10 | + /// <summary>按标签分类的打印量(柱状图)</summary> | |
| 11 | + public List<ReportsCategoryCountDto> LabelsByCategory { get; set; } = new(); | |
| 12 | + | |
| 13 | + /// <summary>折线图:默认统计区间内最后 7 个自然日(按日汇总)</summary> | |
| 14 | + public List<ReportsDailyCountDto> PrintVolumeTrend { get; set; } = new(); | |
| 15 | + | |
| 16 | + /// <summary>用量最高的产品(含占比)</summary> | |
| 17 | + public List<ReportsTopProductRowDto> MostUsedProducts { get; set; } = new(); | |
| 18 | +} | |
| 19 | + | |
| 20 | +public class ReportsLabelReportSummaryDto | |
| 21 | +{ | |
| 22 | + public int TotalLabelsPrinted { get; set; } | |
| 23 | + | |
| 24 | + public int TotalLabelsPrintedPrevPeriod { get; set; } | |
| 25 | + | |
| 26 | + /// <summary>相对上一同长周期变化率(百分比,如 20.1 表示 +20.1%)</summary> | |
| 27 | + public decimal TotalLabelsPrintedChangeRate { get; set; } | |
| 28 | + | |
| 29 | + public string MostPrintedCategoryName { get; set; } = "无"; | |
| 30 | + | |
| 31 | + public int MostPrintedCategoryCount { get; set; } | |
| 32 | + | |
| 33 | + public string TopProductName { get; set; } = "无"; | |
| 34 | + | |
| 35 | + public int TopProductCount { get; set; } | |
| 36 | + | |
| 37 | + public decimal AvgDailyPrints { get; set; } | |
| 38 | + | |
| 39 | + public decimal AvgDailyPrintsPrevPeriod { get; set; } | |
| 40 | + | |
| 41 | + public decimal AvgDailyPrintsChangeRate { get; set; } | |
| 42 | +} | |
| 43 | + | |
| 44 | +public class ReportsCategoryCountDto | |
| 45 | +{ | |
| 46 | + public string CategoryId { get; set; } = string.Empty; | |
| 47 | + | |
| 48 | + public string CategoryName { get; set; } = "无"; | |
| 49 | + | |
| 50 | + public int Count { get; set; } | |
| 51 | +} | |
| 52 | + | |
| 53 | +public class ReportsDailyCountDto | |
| 54 | +{ | |
| 55 | + public string Date { get; set; } = string.Empty; | |
| 56 | + | |
| 57 | + public int Count { get; set; } | |
| 58 | +} | |
| 59 | + | |
| 60 | +public class ReportsTopProductRowDto | |
| 61 | +{ | |
| 62 | + public string ProductId { get; set; } = string.Empty; | |
| 63 | + | |
| 64 | + public string ProductName { get; set; } = "无"; | |
| 65 | + | |
| 66 | + public string CategoryName { get; set; } = "无"; | |
| 67 | + | |
| 68 | + public int TotalPrinted { get; set; } | |
| 69 | + | |
| 70 | + public decimal UsagePercent { get; set; } | |
| 71 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Label Report 统计与导出共用筛选 | |
| 5 | +/// </summary> | |
| 6 | +public class ReportsLabelReportQueryInputVo | |
| 7 | +{ | |
| 8 | + public string? PartnerId { get; set; } | |
| 9 | + | |
| 10 | + public string? GroupId { get; set; } | |
| 11 | + | |
| 12 | + public string? LocationId { get; set; } | |
| 13 | + | |
| 14 | + public DateTime? StartDate { get; set; } | |
| 15 | + | |
| 16 | + public DateTime? EndDate { get; set; } | |
| 17 | + | |
| 18 | + public string? Keyword { get; set; } | |
| 19 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// Web Reports — Print Log 分页查询 | |
| 7 | +/// </summary> | |
| 8 | +public class ReportsPrintLogGetListInputVo : PagedAndSortedResultRequestDto | |
| 9 | +{ | |
| 10 | + public string? PartnerId { get; set; } | |
| 11 | + | |
| 12 | + public string? GroupId { get; set; } | |
| 13 | + | |
| 14 | + public string? LocationId { get; set; } | |
| 15 | + | |
| 16 | + public DateTime? StartDate { get; set; } | |
| 17 | + | |
| 18 | + public DateTime? EndDate { get; set; } | |
| 19 | + | |
| 20 | + public string? Keyword { get; set; } | |
| 21 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Print Log 列表行(Reports 管理端) | |
| 5 | +/// </summary> | |
| 6 | +public class ReportsPrintLogListItemDto | |
| 7 | +{ | |
| 8 | + /// <summary>打印任务 Id(fl_label_print_task.Id),重打时使用</summary> | |
| 9 | + public string TaskId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>标签编码(展示为 Label ID)</summary> | |
| 12 | + public string LabelCode { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public string ProductName { get; set; } = "无"; | |
| 15 | + | |
| 16 | + /// <summary>分类展示名(优先产品分类,否则标签分类)</summary> | |
| 17 | + public string CategoryName { get; set; } = "无"; | |
| 18 | + | |
| 19 | + /// <summary>模板展示(尺寸 + 模板名)</summary> | |
| 20 | + public string TemplateText { get; set; } = "无"; | |
| 21 | + | |
| 22 | + public DateTime PrintedAt { get; set; } | |
| 23 | + | |
| 24 | + /// <summary>打印人姓名</summary> | |
| 25 | + public string PrintedByName { get; set; } = "无"; | |
| 26 | + | |
| 27 | + /// <summary>门店展示:名称 (编码)</summary> | |
| 28 | + public string LocationText { get; set; } = "无"; | |
| 29 | + | |
| 30 | + /// <summary>门店 Id(重打校验用)</summary> | |
| 31 | + public string? LocationId { get; set; } | |
| 32 | + | |
| 33 | + /// <summary>从 PrintInputJson 尽力解析的保质期文本;无则「无」</summary> | |
| 34 | + public string ExpiryDateText { get; set; } = "无"; | |
| 35 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App 修改密码入参 | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppChangePasswordInputVo | |
| 7 | +{ | |
| 8 | + /// <summary>当前密码</summary> | |
| 9 | + public string CurrentPassword { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>新密码</summary> | |
| 12 | + public string NewPassword { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>确认新密码</summary> | |
| 15 | + public string ConfirmNewPassword { get; set; } = string.Empty; | |
| 16 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App「Location」门店详情(与原型字段对齐) | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppLocationDetailOutputDto | |
| 7 | +{ | |
| 8 | + /// <summary>门店主键(Guid 字符串)</summary> | |
| 9 | + public string LocationId { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>门店名称</summary> | |
| 12 | + public string LocationName { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>完整地址(街道、城市、州、邮编拼接;无则为「无」)</summary> | |
| 15 | + public string FullAddress { get; set; } = string.Empty; | |
| 16 | + | |
| 17 | + /// <summary>门店电话(来自 location.Phone;空为「无」)</summary> | |
| 18 | + public string StorePhone { get; set; } = string.Empty; | |
| 19 | + | |
| 20 | + /// <summary>营业时间;当前库无字段,固定返回「无」直至业务落库</summary> | |
| 21 | + public string OperatingHours { get; set; } = string.Empty; | |
| 22 | + | |
| 23 | + /// <summary>店长姓名;优先取绑定本店且角色名/编码含 manager 的用户</summary> | |
| 24 | + public string ManagerName { get; set; } = string.Empty; | |
| 25 | + | |
| 26 | + /// <summary>店长电话;同上用户 User.Phone 格式化;无则为「无」</summary> | |
| 27 | + public string ManagerPhone { get; set; } = string.Empty; | |
| 28 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App「我的资料」展示数据(My Profile) | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppMyProfileOutputDto | |
| 7 | +{ | |
| 8 | + /// <summary>全名(Name,无则 Nick,再无则 UserName)</summary> | |
| 9 | + public string FullName { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>邮箱</summary> | |
| 12 | + public string Email { get; set; } = "无"; | |
| 13 | + | |
| 14 | + /// <summary>电话展示(如 +1 (555) 123-4567);无则「无」</summary> | |
| 15 | + public string Phone { get; set; } = "无"; | |
| 16 | + | |
| 17 | + /// <summary>员工号/登录名(当前使用 <c>User.UserName</c>,可与业务约定为工号)</summary> | |
| 18 | + public string EmployeeId { get; set; } = string.Empty; | |
| 19 | + | |
| 20 | + /// <summary>角色展示名(多角色英文逗号拼接,按角色 OrderNum)</summary> | |
| 21 | + public string RoleDisplay { get; set; } = "无"; | |
| 22 | + | |
| 23 | + /// <summary>主角色编码(第一个角色 RoleCode,供前端样式)</summary> | |
| 24 | + public string? PrimaryRoleCode { get; set; } | |
| 25 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs
| ... | ... | @@ -9,7 +9,7 @@ namespace FoodLabeling.Application.Contracts.IServices; |
| 9 | 9 | public interface IDashboardAppService : IApplicationService |
| 10 | 10 | { |
| 11 | 11 | /// <summary> |
| 12 | - /// 获取 Dashboard 总览统计(卡片 + 周趋势 + 分类分布) | |
| 12 | + /// 获取 Dashboard 总览统计(卡片 + 周趋势 + 分类分布 + Recent Labels 最近打印) | |
| 13 | 13 | /// </summary> |
| 14 | 14 | Task<DashboardOverviewOutputDto> GetOverviewAsync(); |
| 15 | 15 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Group; | |
| 3 | +using Microsoft.AspNetCore.Mvc; | |
| 4 | +using Volo.Abp.Application.Dtos; | |
| 5 | +using Volo.Abp.Application.Services; | |
| 6 | + | |
| 7 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// 组织(Group)管理接口(fl_group) | |
| 11 | +/// </summary> | |
| 12 | +public interface IGroupAppService : IApplicationService | |
| 13 | +{ | |
| 14 | + /// <summary> | |
| 15 | + /// 组织分页列表(与导出使用相同筛选条件) | |
| 16 | + /// </summary> | |
| 17 | + /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> | |
| 18 | + Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input); | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 组织详情 | |
| 22 | + /// </summary> | |
| 23 | + Task<GroupGetOutputDto> GetAsync(string id); | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 新增组织 | |
| 27 | + /// </summary> | |
| 28 | + Task<GroupGetOutputDto> CreateAsync(GroupCreateInputVo input); | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 编辑组织 | |
| 32 | + /// </summary> | |
| 33 | + Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input); | |
| 34 | + | |
| 35 | + /// <summary> | |
| 36 | + /// 删除组织(逻辑删除) | |
| 37 | + /// </summary> | |
| 38 | + Task DeleteAsync(string id); | |
| 39 | + | |
| 40 | + /// <summary> | |
| 41 | + /// 按列表相同筛选条件导出组织为 PDF(上限 5000 条) | |
| 42 | + /// </summary> | |
| 43 | + Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input); | |
| 44 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 3 | +using Microsoft.AspNetCore.Mvc; | |
| 4 | +using Volo.Abp.Application.Dtos; | |
| 5 | +using Volo.Abp.Application.Services; | |
| 6 | + | |
| 7 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// 合作伙伴管理接口(fl_partner) | |
| 11 | +/// </summary> | |
| 12 | +public interface IPartnerAppService : IApplicationService | |
| 13 | +{ | |
| 14 | + /// <summary> | |
| 15 | + /// 合作伙伴分页列表(与导出使用相同筛选条件) | |
| 16 | + /// </summary> | |
| 17 | + /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> | |
| 18 | + /// <returns>分页数据</returns> | |
| 19 | + /// <response code="200">成功</response> | |
| 20 | + /// <response code="400">参数错误</response> | |
| 21 | + /// <response code="500">服务器错误</response> | |
| 22 | + Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input); | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 合作伙伴详情 | |
| 26 | + /// </summary> | |
| 27 | + /// <param name="id">主键 Id</param> | |
| 28 | + /// <returns>详情</returns> | |
| 29 | + /// <response code="200">成功</response> | |
| 30 | + /// <response code="400">Id 无效</response> | |
| 31 | + /// <response code="500">服务器错误</response> | |
| 32 | + Task<PartnerGetOutputDto> GetAsync(string id); | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 新增合作伙伴 | |
| 36 | + /// </summary> | |
| 37 | + /// <param name="input">名称、邮箱、电话、启用状态</param> | |
| 38 | + /// <returns>新建后的详情</returns> | |
| 39 | + /// <remarks> | |
| 40 | + /// 示例请求: | |
| 41 | + /// ```json | |
| 42 | + /// { | |
| 43 | + /// "partnerName": "Global Foods Inc.", | |
| 44 | + /// "contactEmail": "admin@globalfoods.com", | |
| 45 | + /// "phoneNumber": "+1 (555) 100-2000", | |
| 46 | + /// "state": true | |
| 47 | + /// } | |
| 48 | + /// ``` | |
| 49 | + /// </remarks> | |
| 50 | + /// <response code="200">成功</response> | |
| 51 | + /// <response code="400">校验失败</response> | |
| 52 | + /// <response code="500">服务器错误</response> | |
| 53 | + Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input); | |
| 54 | + | |
| 55 | + /// <summary> | |
| 56 | + /// 编辑合作伙伴 | |
| 57 | + /// </summary> | |
| 58 | + /// <param name="id">主键 Id</param> | |
| 59 | + /// <param name="input">名称、邮箱、电话、启用状态</param> | |
| 60 | + /// <returns>更新后的详情</returns> | |
| 61 | + /// <response code="200">成功</response> | |
| 62 | + /// <response code="400">校验失败或记录不存在</response> | |
| 63 | + /// <response code="500">服务器错误</response> | |
| 64 | + Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input); | |
| 65 | + | |
| 66 | + /// <summary> | |
| 67 | + /// 删除合作伙伴(逻辑删除) | |
| 68 | + /// </summary> | |
| 69 | + /// <param name="id">主键 Id</param> | |
| 70 | + /// <response code="200">成功</response> | |
| 71 | + /// <response code="400">Id 无效或记录不存在</response> | |
| 72 | + /// <response code="500">服务器错误</response> | |
| 73 | + Task DeleteAsync(string id); | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// 按当前列表筛选条件批量导出合作伙伴为 PDF(不分页,上限 5000 条) | |
| 77 | + /// </summary> | |
| 78 | + /// <param name="input">与列表相同的 Keyword、State;分页字段忽略</param> | |
| 79 | + /// <returns>PDF 文件流</returns> | |
| 80 | + /// <remarks> | |
| 81 | + /// 筛选条件需与 <see cref="GetListAsync"/> 一致,便于统计与导出数据对齐。 | |
| 82 | + /// </remarks> | |
| 83 | + /// <response code="200">成功返回 application/pdf</response> | |
| 84 | + /// <response code="400">参数错误</response> | |
| 85 | + /// <response code="500">服务器错误</response> | |
| 86 | + Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input); | |
| 87 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | |
| 4 | +using Microsoft.AspNetCore.Mvc; | |
| 5 | +using Volo.Abp.Application.Services; | |
| 6 | + | |
| 7 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 8 | + | |
| 9 | +/// <summary> | |
| 10 | +/// Reports(Print Log / Label Report)管理端接口 | |
| 11 | +/// </summary> | |
| 12 | +public interface IReportsAppService : IApplicationService | |
| 13 | +{ | |
| 14 | + /// <summary> | |
| 15 | + /// Print Log 分页列表;角色 <c>admin</c> 可查全部,否则仅当前用户打印记录。 | |
| 16 | + /// </summary> | |
| 17 | + Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync(ReportsPrintLogGetListInputVo input); | |
| 18 | + | |
| 19 | + /// <summary> | |
| 20 | + /// Print Log 导出 PDF(筛选与列表一致,最多 5000 条) | |
| 21 | + /// </summary> | |
| 22 | + Task<IActionResult> ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input); | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 根据历史任务重打(与 App 入参一致);<c>admin</c> 可重打任意用户任务,否则仅本人任务。 | |
| 26 | + /// </summary> | |
| 27 | + Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input); | |
| 28 | + | |
| 29 | + /// <summary> | |
| 30 | + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);<c>admin</c> 统计全部,否则仅当前用户。 | |
| 31 | + /// </summary> | |
| 32 | + Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input); | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// Label Report 导出 PDF | |
| 36 | + /// </summary> | |
| 37 | + Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input); | |
| 38 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
| ... | ... | @@ -17,4 +17,35 @@ public interface IUsAppAuthAppService : IApplicationService |
| 17 | 17 | /// 获取当前登录账号已绑定的门店(用于切换门店等场景) |
| 18 | 18 | /// </summary> |
| 19 | 19 | Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync(); |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 获取当前登录用户资料(My Profile:姓名、邮箱、电话、员工号、角色) | |
| 23 | + /// </summary> | |
| 24 | + /// <returns>资料 DTO</returns> | |
| 25 | + /// <response code="200">成功</response> | |
| 26 | + /// <response code="400">未登录或用户不存在</response> | |
| 27 | + /// <response code="500">服务器错误</response> | |
| 28 | + Task<UsAppMyProfileOutputDto> GetMyProfileAsync(); | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 当前登录用户修改密码(校验原密码与复杂度规则) | |
| 32 | + /// </summary> | |
| 33 | + /// <param name="input">当前密码、新密码、确认密码</param> | |
| 34 | + /// <remarks> | |
| 35 | + /// 新密码需满足:至少 8 位;含大写与小写字母;至少 1 位数字;至少 1 个非字母数字特殊字符。 | |
| 36 | + /// </remarks> | |
| 37 | + /// <response code="200">成功</response> | |
| 38 | + /// <response code="400">参数或校验失败</response> | |
| 39 | + /// <response code="500">服务器错误</response> | |
| 40 | + Task ChangePasswordAsync(UsAppChangePasswordInputVo input); | |
| 41 | + | |
| 42 | + /// <summary> | |
| 43 | + /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) | |
| 44 | + /// </summary> | |
| 45 | + /// <param name="locationId">门店 Guid 字符串</param> | |
| 46 | + /// <returns>店名、地址、电话、营业时间占位、店长信息</returns> | |
| 47 | + /// <response code="200">成功</response> | |
| 48 | + /// <response code="400">参数非法、未绑定或无权限</response> | |
| 49 | + /// <response code="500">服务器错误</response> | |
| 50 | + Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId); | |
| 20 | 51 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs
0 → 100644
| 1 | +using Volo.Abp.Users; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Helpers; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致) | |
| 7 | +/// </summary> | |
| 8 | +public static class ReportsRoleHelper | |
| 9 | +{ | |
| 10 | + /// <summary> | |
| 11 | + /// 是否为管理员:任一角色码等于 <c>admin</c>(忽略大小写)则视为可查看全部打印数据。 | |
| 12 | + /// </summary> | |
| 13 | + public static bool IsAdminRole(ICurrentUser currentUser) | |
| 14 | + { | |
| 15 | + if (currentUser.Roles is null) | |
| 16 | + { | |
| 17 | + return false; | |
| 18 | + } | |
| 19 | + | |
| 20 | + foreach (var r in currentUser.Roles) | |
| 21 | + { | |
| 22 | + if (!string.IsNullOrWhiteSpace(r) && | |
| 23 | + string.Equals(r.Trim(), "admin", StringComparison.OrdinalIgnoreCase)) | |
| 24 | + { | |
| 25 | + return true; | |
| 26 | + } | |
| 27 | + } | |
| 28 | + | |
| 29 | + return false; | |
| 30 | + } | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs
| 1 | +using System.Globalization; | |
| 2 | +using System.Text.Json; | |
| 1 | 3 | using FoodLabeling.Application.Contracts.Dtos.Dashboard; |
| 2 | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 3 | 5 | using FoodLabeling.Application.Services.DbModels; |
| 4 | 6 | using FoodLabeling.Domain.Entities; |
| 7 | +using SqlSugar; | |
| 5 | 8 | using Volo.Abp.Application.Services; |
| 6 | 9 | using Yi.Framework.Rbac.Domain.Entities; |
| 7 | 10 | using Yi.Framework.SqlSugarCore.Abstractions; |
| ... | ... | @@ -127,6 +130,72 @@ public class DashboardAppService : ApplicationService, IDashboardAppService |
| 127 | 130 | .ThenBy(x => x.CategoryName) |
| 128 | 131 | .ToList(); |
| 129 | 132 | |
| 133 | + const int recentLabelsTake = 10; | |
| 134 | + var recentRaw = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 135 | + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) | |
| 136 | + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id) | |
| 137 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, tpl) => t.TemplateId == tpl.Id) | |
| 138 | + .OrderBy((t, l, p, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) | |
| 139 | + .Take(recentLabelsTake) | |
| 140 | + .Select((t, l, p, tpl) => new | |
| 141 | + { | |
| 142 | + t.Id, | |
| 143 | + LabelCode = l.LabelCode, | |
| 144 | + LabelName = l.LabelName, | |
| 145 | + ProductName = p.ProductName, | |
| 146 | + tpl.Width, | |
| 147 | + tpl.Height, | |
| 148 | + tpl.Unit, | |
| 149 | + t.PrintInputJson, | |
| 150 | + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 151 | + t.CreatedBy | |
| 152 | + }) | |
| 153 | + .ToListAsync(); | |
| 154 | + | |
| 155 | + var recentUserIds = recentRaw | |
| 156 | + .Select(x => x.CreatedBy) | |
| 157 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 158 | + .Select(x => x!.Trim()) | |
| 159 | + .Distinct() | |
| 160 | + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) | |
| 161 | + .Where(x => x.HasValue) | |
| 162 | + .Select(x => x!.Value) | |
| 163 | + .ToList(); | |
| 164 | + | |
| 165 | + var recentUserMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| 166 | + if (recentUserIds.Count > 0) | |
| 167 | + { | |
| 168 | + var users = await _userRepository._DbQueryable | |
| 169 | + .Where(u => !u.IsDeleted && recentUserIds.Contains(u.Id)) | |
| 170 | + .Select(u => new { u.Id, u.Name, u.UserName }) | |
| 171 | + .ToListAsync(); | |
| 172 | + foreach (var u in users) | |
| 173 | + { | |
| 174 | + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); | |
| 175 | + recentUserMap[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; | |
| 176 | + } | |
| 177 | + } | |
| 178 | + | |
| 179 | + var recentLabels = recentRaw.Select(x => | |
| 180 | + { | |
| 181 | + var displayName = !string.IsNullOrWhiteSpace(x.ProductName) | |
| 182 | + ? x.ProductName.Trim() | |
| 183 | + : (string.IsNullOrWhiteSpace(x.LabelName) ? "无" : x.LabelName.Trim()); | |
| 184 | + var printedAt = x.PrintedAt ?? DateTime.MinValue; | |
| 185 | + var status = ResolveRecentLabelStatus(x.PrintInputJson); | |
| 186 | + return new DashboardRecentLabelItemDto | |
| 187 | + { | |
| 188 | + TaskId = x.Id, | |
| 189 | + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), | |
| 190 | + DisplayName = displayName, | |
| 191 | + PrintedByUserId = x.CreatedBy?.Trim(), | |
| 192 | + PrintedByName = ResolveRecentUserName(recentUserMap, x.CreatedBy), | |
| 193 | + PrintedAt = printedAt, | |
| 194 | + Status = status, | |
| 195 | + LabelTypeBadge = FormatTemplateBadge(x.Width, x.Height, x.Unit) | |
| 196 | + }; | |
| 197 | + }).ToList(); | |
| 198 | + | |
| 130 | 199 | var labelsPrintedTodayCard = BuildMetricCard("labelsPrintedToday", "Labels Printed Today", printedToday, printedYesterday); |
| 131 | 200 | var activeTemplatesCard = BuildMetricCard("activeTemplates", "Active Templates", activeTemplates, activeTemplatesPrevWeek); |
| 132 | 201 | var activeUsersCard = BuildMetricCard("activeUsers", "Active Users", activeUsers, activeUsersPrevWeek); |
| ... | ... | @@ -156,12 +225,100 @@ public class DashboardAppService : ApplicationService, IDashboardAppService |
| 156 | 225 | CategoryDistributionTotal = categoryDistributionTotal, |
| 157 | 226 | ByCategory = categoryDistribution, |
| 158 | 227 | ByCategoryTotal = categoryDistributionTotal, |
| 228 | + RecentLabels = recentLabels, | |
| 159 | 229 | GeneratedAt = now |
| 160 | 230 | }; |
| 161 | 231 | |
| 162 | 232 | return output; |
| 163 | 233 | } |
| 164 | 234 | |
| 235 | + private static string ResolveRecentUserName(Dictionary<string, string> map, string? createdBy) | |
| 236 | + { | |
| 237 | + if (string.IsNullOrWhiteSpace(createdBy)) | |
| 238 | + { | |
| 239 | + return "无"; | |
| 240 | + } | |
| 241 | + | |
| 242 | + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; | |
| 243 | + } | |
| 244 | + | |
| 245 | + /// <summary> | |
| 246 | + /// 依据 PrintInputJson 中的保质期字段与「当前日期」比较得到 active/expired。 | |
| 247 | + /// </summary> | |
| 248 | + private static string ResolveRecentLabelStatus(string? printInputJson) | |
| 249 | + { | |
| 250 | + if (!TryParseExpiryDate(printInputJson, out var expiryDate)) | |
| 251 | + { | |
| 252 | + return "active"; | |
| 253 | + } | |
| 254 | + | |
| 255 | + var today = DateTime.Now.Date; | |
| 256 | + return expiryDate.Date < today ? "expired" : "active"; | |
| 257 | + } | |
| 258 | + | |
| 259 | + private static bool TryParseExpiryDate(string? printInputJson, out DateTime expiryDate) | |
| 260 | + { | |
| 261 | + expiryDate = default; | |
| 262 | + if (string.IsNullOrWhiteSpace(printInputJson)) | |
| 263 | + { | |
| 264 | + return false; | |
| 265 | + } | |
| 266 | + | |
| 267 | + try | |
| 268 | + { | |
| 269 | + using var doc = JsonDocument.Parse(printInputJson); | |
| 270 | + if (doc.RootElement.ValueKind != JsonValueKind.Object) | |
| 271 | + { | |
| 272 | + return false; | |
| 273 | + } | |
| 274 | + | |
| 275 | + foreach (var prop in doc.RootElement.EnumerateObject()) | |
| 276 | + { | |
| 277 | + var key = prop.Name.Trim(); | |
| 278 | + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && | |
| 279 | + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && | |
| 280 | + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) | |
| 281 | + { | |
| 282 | + continue; | |
| 283 | + } | |
| 284 | + | |
| 285 | + var v = prop.Value; | |
| 286 | + if (v.ValueKind == JsonValueKind.String) | |
| 287 | + { | |
| 288 | + var s = v.GetString(); | |
| 289 | + if (!string.IsNullOrWhiteSpace(s) && | |
| 290 | + DateTime.TryParse(s, CultureInfo.InvariantCulture, | |
| 291 | + DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces, out var dt)) | |
| 292 | + { | |
| 293 | + expiryDate = dt; | |
| 294 | + return true; | |
| 295 | + } | |
| 296 | + } | |
| 297 | + else if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var unix)) | |
| 298 | + { | |
| 299 | + expiryDate = DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime; | |
| 300 | + return true; | |
| 301 | + } | |
| 302 | + } | |
| 303 | + } | |
| 304 | + catch | |
| 305 | + { | |
| 306 | + return false; | |
| 307 | + } | |
| 308 | + | |
| 309 | + return false; | |
| 310 | + } | |
| 311 | + | |
| 312 | + private static string FormatTemplateBadge(decimal w, decimal h, string? unit) | |
| 313 | + { | |
| 314 | + var u = (unit ?? "inch").Trim().ToLowerInvariant(); | |
| 315 | + var ws = w.ToString(CultureInfo.InvariantCulture); | |
| 316 | + var hs = h.ToString(CultureInfo.InvariantCulture); | |
| 317 | + return u is "inch" or "in" | |
| 318 | + ? $"{ws}\"x{hs}\"" | |
| 319 | + : $"{ws}x{hs}{u}"; | |
| 320 | + } | |
| 321 | + | |
| 165 | 322 | private static DashboardMetricCardDto BuildMetricCard(string key, string title, int value, int previousValue) |
| 166 | 323 | { |
| 167 | 324 | var changeValue = value - previousValue; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 组织/分组(Account Management / Group,表 fl_group) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_group")] | |
| 9 | +public class FlGroupDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public bool IsDeleted { get; set; } | |
| 15 | + | |
| 16 | + public DateTime CreationTime { get; set; } | |
| 17 | + | |
| 18 | + public string? CreatorId { get; set; } | |
| 19 | + | |
| 20 | + public string? LastModifierId { get; set; } | |
| 21 | + | |
| 22 | + public DateTime? LastModificationTime { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 组织名称(Group Name) | |
| 26 | + /// </summary> | |
| 27 | + public string GroupName { get; set; } = string.Empty; | |
| 28 | + | |
| 29 | + /// <summary> | |
| 30 | + /// 所属合作伙伴 Id(fl_partner.Id) | |
| 31 | + /// </summary> | |
| 32 | + public string PartnerId { get; set; } = string.Empty; | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 是否启用(对应 UI Active) | |
| 36 | + /// </summary> | |
| 37 | + public bool State { get; set; } | |
| 38 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +/// <summary> | |
| 6 | +/// 合作伙伴主数据(Account Management / Partner,表 fl_partner) | |
| 7 | +/// </summary> | |
| 8 | +[SugarTable("fl_partner")] | |
| 9 | +public class FlPartnerDbEntity | |
| 10 | +{ | |
| 11 | + [SugarColumn(IsPrimaryKey = true)] | |
| 12 | + public string Id { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + public bool IsDeleted { get; set; } | |
| 15 | + | |
| 16 | + public DateTime CreationTime { get; set; } | |
| 17 | + | |
| 18 | + public string? CreatorId { get; set; } | |
| 19 | + | |
| 20 | + public string? LastModifierId { get; set; } | |
| 21 | + | |
| 22 | + public DateTime? LastModificationTime { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 合作伙伴名称(公司名) | |
| 26 | + /// </summary> | |
| 27 | + public string PartnerName { get; set; } = string.Empty; | |
| 28 | + | |
| 29 | + /// <summary> | |
| 30 | + /// 联系邮箱 | |
| 31 | + /// </summary> | |
| 32 | + public string? ContactEmail { get; set; } | |
| 33 | + | |
| 34 | + /// <summary> | |
| 35 | + /// 电话 | |
| 36 | + /// </summary> | |
| 37 | + public string? PhoneNumber { get; set; } | |
| 38 | + | |
| 39 | + /// <summary> | |
| 40 | + /// 是否启用(对应 UI Active) | |
| 41 | + /// </summary> | |
| 42 | + public bool State { get; set; } | |
| 43 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Helpers; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.Group; | |
| 4 | +using FoodLabeling.Application.Contracts.IServices; | |
| 5 | +using FoodLabeling.Application.Services.DbModels; | |
| 6 | +using Microsoft.AspNetCore.Mvc; | |
| 7 | +using QuestPDF.Fluent; | |
| 8 | +using QuestPDF.Helpers; | |
| 9 | +using QuestPDF.Infrastructure; | |
| 10 | +using SqlSugar; | |
| 11 | +using Volo.Abp; | |
| 12 | +using Volo.Abp.Application.Services; | |
| 13 | +using Volo.Abp.Guids; | |
| 14 | +using Volo.Abp.Uow; | |
| 15 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 16 | + | |
| 17 | +namespace FoodLabeling.Application.Services; | |
| 18 | + | |
| 19 | +/// <summary> | |
| 20 | +/// 组织(Group)管理(fl_group) | |
| 21 | +/// </summary> | |
| 22 | +public class GroupAppService : ApplicationService, IGroupAppService | |
| 23 | +{ | |
| 24 | + private const int ExportPdfMaxRows = 5000; | |
| 25 | + | |
| 26 | + private readonly ISqlSugarDbContext _dbContext; | |
| 27 | + private readonly IGuidGenerator _guidGenerator; | |
| 28 | + | |
| 29 | + public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | |
| 30 | + { | |
| 31 | + _dbContext = dbContext; | |
| 32 | + _guidGenerator = guidGenerator; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /// <inheritdoc /> | |
| 36 | + public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input) | |
| 37 | + { | |
| 38 | + RefAsync<int> total = 0; | |
| 39 | + var query = BuildGroupJoinedQuery(input); | |
| 40 | + var projected = query.Select((g, p) => new GroupGetListOutputDto | |
| 41 | + { | |
| 42 | + Id = g.Id, | |
| 43 | + GroupName = g.GroupName, | |
| 44 | + PartnerId = g.PartnerId, | |
| 45 | + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), | |
| 46 | + State = g.State, | |
| 47 | + CreationTime = g.CreationTime | |
| 48 | + }); | |
| 49 | + | |
| 50 | + var items = await projected.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | |
| 51 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | |
| 52 | + } | |
| 53 | + | |
| 54 | + /// <inheritdoc /> | |
| 55 | + public async Task<GroupGetOutputDto> GetAsync(string id) | |
| 56 | + { | |
| 57 | + var groupId = id?.Trim(); | |
| 58 | + if (string.IsNullOrWhiteSpace(groupId)) | |
| 59 | + { | |
| 60 | + throw new UserFriendlyException("组织Id不能为空"); | |
| 61 | + } | |
| 62 | + | |
| 63 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 64 | + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | |
| 65 | + if (entity is null) | |
| 66 | + { | |
| 67 | + throw new UserFriendlyException("组织不存在"); | |
| 68 | + } | |
| 69 | + | |
| 70 | + var partnerName = await ResolvePartnerNameAsync(entity.PartnerId); | |
| 71 | + return MapDetail(entity, partnerName); | |
| 72 | + } | |
| 73 | + | |
| 74 | + /// <inheritdoc /> | |
| 75 | + [UnitOfWork] | |
| 76 | + public async Task<GroupGetOutputDto> CreateAsync(GroupCreateInputVo input) | |
| 77 | + { | |
| 78 | + var name = input.GroupName?.Trim(); | |
| 79 | + if (string.IsNullOrWhiteSpace(name)) | |
| 80 | + { | |
| 81 | + throw new UserFriendlyException("组织名称不能为空"); | |
| 82 | + } | |
| 83 | + | |
| 84 | + var partnerId = input.PartnerId?.Trim(); | |
| 85 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 86 | + { | |
| 87 | + throw new UserFriendlyException("请选择所属合作伙伴"); | |
| 88 | + } | |
| 89 | + | |
| 90 | + await EnsurePartnerExistsAsync(partnerId); | |
| 91 | + | |
| 92 | + var now = Clock.Now; | |
| 93 | + var entity = new FlGroupDbEntity | |
| 94 | + { | |
| 95 | + Id = _guidGenerator.Create().ToString(), | |
| 96 | + IsDeleted = false, | |
| 97 | + GroupName = name, | |
| 98 | + PartnerId = partnerId, | |
| 99 | + State = input.State, | |
| 100 | + CreationTime = now, | |
| 101 | + CreatorId = CurrentUser?.Id?.ToString(), | |
| 102 | + LastModificationTime = now, | |
| 103 | + LastModifierId = CurrentUser?.Id?.ToString() | |
| 104 | + }; | |
| 105 | + | |
| 106 | + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | |
| 107 | + return await GetAsync(entity.Id); | |
| 108 | + } | |
| 109 | + | |
| 110 | + /// <inheritdoc /> | |
| 111 | + [UnitOfWork] | |
| 112 | + public async Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input) | |
| 113 | + { | |
| 114 | + var groupId = id?.Trim(); | |
| 115 | + if (string.IsNullOrWhiteSpace(groupId)) | |
| 116 | + { | |
| 117 | + throw new UserFriendlyException("组织Id不能为空"); | |
| 118 | + } | |
| 119 | + | |
| 120 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 121 | + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | |
| 122 | + if (entity is null) | |
| 123 | + { | |
| 124 | + throw new UserFriendlyException("组织不存在"); | |
| 125 | + } | |
| 126 | + | |
| 127 | + var name = input.GroupName?.Trim(); | |
| 128 | + if (string.IsNullOrWhiteSpace(name)) | |
| 129 | + { | |
| 130 | + throw new UserFriendlyException("组织名称不能为空"); | |
| 131 | + } | |
| 132 | + | |
| 133 | + var partnerId = input.PartnerId?.Trim(); | |
| 134 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 135 | + { | |
| 136 | + throw new UserFriendlyException("请选择所属合作伙伴"); | |
| 137 | + } | |
| 138 | + | |
| 139 | + await EnsurePartnerExistsAsync(partnerId); | |
| 140 | + | |
| 141 | + entity.GroupName = name; | |
| 142 | + entity.PartnerId = partnerId; | |
| 143 | + entity.State = input.State; | |
| 144 | + entity.LastModificationTime = Clock.Now; | |
| 145 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 146 | + | |
| 147 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 148 | + return await GetAsync(groupId); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /// <inheritdoc /> | |
| 152 | + [UnitOfWork] | |
| 153 | + public async Task DeleteAsync(string id) | |
| 154 | + { | |
| 155 | + var groupId = id?.Trim(); | |
| 156 | + if (string.IsNullOrWhiteSpace(groupId)) | |
| 157 | + { | |
| 158 | + throw new UserFriendlyException("组织Id不能为空"); | |
| 159 | + } | |
| 160 | + | |
| 161 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 162 | + .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | |
| 163 | + if (entity is null) | |
| 164 | + { | |
| 165 | + throw new UserFriendlyException("组织不存在"); | |
| 166 | + } | |
| 167 | + | |
| 168 | + entity.IsDeleted = true; | |
| 169 | + entity.LastModificationTime = Clock.Now; | |
| 170 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 171 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 172 | + } | |
| 173 | + | |
| 174 | + /// <inheritdoc /> | |
| 175 | + public async Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input) | |
| 176 | + { | |
| 177 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 178 | + | |
| 179 | + var exportBase = BuildGroupJoinedQuery(input); | |
| 180 | + var count = await exportBase.CountAsync(); | |
| 181 | + if (count > ExportPdfMaxRows) | |
| 182 | + { | |
| 183 | + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | |
| 184 | + } | |
| 185 | + | |
| 186 | + var rows = await BuildGroupJoinedQuery(input) | |
| 187 | + .Select((g, p) => new GroupGetListOutputDto | |
| 188 | + { | |
| 189 | + Id = g.Id, | |
| 190 | + GroupName = g.GroupName, | |
| 191 | + PartnerId = g.PartnerId, | |
| 192 | + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), | |
| 193 | + State = g.State, | |
| 194 | + CreationTime = g.CreationTime | |
| 195 | + }) | |
| 196 | + .Take(ExportPdfMaxRows) | |
| 197 | + .ToListAsync(); | |
| 198 | + | |
| 199 | + var fileName = $"groups_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 200 | + var document = Document.Create(container => | |
| 201 | + { | |
| 202 | + container.Page(page => | |
| 203 | + { | |
| 204 | + page.Margin(28); | |
| 205 | + page.DefaultTextStyle(x => x.FontSize(10)); | |
| 206 | + page.Header().Text("Groups").SemiBold().FontSize(18); | |
| 207 | + page.Content().PaddingTop(12).Table(table => | |
| 208 | + { | |
| 209 | + table.ColumnsDefinition(c => | |
| 210 | + { | |
| 211 | + c.RelativeColumn(2.2f); | |
| 212 | + c.RelativeColumn(2.4f); | |
| 213 | + c.RelativeColumn(1f); | |
| 214 | + c.RelativeColumn(1.6f); | |
| 215 | + }); | |
| 216 | + | |
| 217 | + static IContainer CellHeader(IContainer c) => | |
| 218 | + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold()); | |
| 219 | + | |
| 220 | + table.Cell().Element(CellHeader).Text("Group Name"); | |
| 221 | + table.Cell().Element(CellHeader).Text("Parent Partner"); | |
| 222 | + table.Cell().Element(CellHeader).Text("Status"); | |
| 223 | + table.Cell().Element(CellHeader).Text("Created"); | |
| 224 | + | |
| 225 | + foreach (var e in rows) | |
| 226 | + { | |
| 227 | + var status = e.State ? "active" : "inactive"; | |
| 228 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 229 | + .Text(e.GroupName ?? string.Empty); | |
| 230 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 231 | + .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "无" : e.PartnerName); | |
| 232 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); | |
| 233 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 234 | + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); | |
| 235 | + } | |
| 236 | + }); | |
| 237 | + }); | |
| 238 | + }); | |
| 239 | + | |
| 240 | + var stream = new MemoryStream(); | |
| 241 | + document.GeneratePdf(stream); | |
| 242 | + stream.Position = 0; | |
| 243 | + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; | |
| 244 | + } | |
| 245 | + | |
| 246 | + private ISugarQueryable<FlGroupDbEntity, FlPartnerDbEntity> BuildGroupJoinedQuery(GroupGetListInputVo input) | |
| 247 | + { | |
| 248 | + var keyword = input.Keyword?.Trim(); | |
| 249 | + var partnerId = input.PartnerId?.Trim(); | |
| 250 | + | |
| 251 | + var query = _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 252 | + .LeftJoin<FlPartnerDbEntity>((g, p) => g.PartnerId == p.Id && !p.IsDeleted) | |
| 253 | + .Where((g, p) => !g.IsDeleted) | |
| 254 | + .WhereIF(input.State != null, (g, p) => g.State == input.State) | |
| 255 | + .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId) | |
| 256 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), | |
| 257 | + (g, p) => g.GroupName.Contains(keyword!) || | |
| 258 | + (p.PartnerName != null && p.PartnerName.Contains(keyword!))); | |
| 259 | + | |
| 260 | + if (!string.IsNullOrWhiteSpace(input.Sorting)) | |
| 261 | + { | |
| 262 | + var sorting = input.Sorting.Trim(); | |
| 263 | + if (sorting.Equals("GroupName desc", StringComparison.OrdinalIgnoreCase)) | |
| 264 | + { | |
| 265 | + query = query.OrderByDescending((g, p) => g.GroupName); | |
| 266 | + } | |
| 267 | + else if (sorting.Equals("GroupName asc", StringComparison.OrdinalIgnoreCase)) | |
| 268 | + { | |
| 269 | + query = query.OrderBy((g, p) => g.GroupName); | |
| 270 | + } | |
| 271 | + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) | |
| 272 | + { | |
| 273 | + query = query.OrderByDescending((g, p) => g.CreationTime); | |
| 274 | + } | |
| 275 | + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) | |
| 276 | + { | |
| 277 | + query = query.OrderBy((g, p) => g.CreationTime); | |
| 278 | + } | |
| 279 | + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase)) | |
| 280 | + { | |
| 281 | + query = query.OrderByDescending((g, p) => g.State); | |
| 282 | + } | |
| 283 | + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase)) | |
| 284 | + { | |
| 285 | + query = query.OrderBy((g, p) => g.State); | |
| 286 | + } | |
| 287 | + else if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase)) | |
| 288 | + { | |
| 289 | + query = query.OrderByDescending((g, p) => p.PartnerName); | |
| 290 | + } | |
| 291 | + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase)) | |
| 292 | + { | |
| 293 | + query = query.OrderBy((g, p) => p.PartnerName); | |
| 294 | + } | |
| 295 | + else | |
| 296 | + { | |
| 297 | + query = query.OrderByDescending((g, p) => g.CreationTime); | |
| 298 | + } | |
| 299 | + } | |
| 300 | + else | |
| 301 | + { | |
| 302 | + query = query.OrderByDescending((g, p) => g.CreationTime); | |
| 303 | + } | |
| 304 | + | |
| 305 | + return query; | |
| 306 | + } | |
| 307 | + | |
| 308 | + private async Task EnsurePartnerExistsAsync(string partnerId) | |
| 309 | + { | |
| 310 | + var ok = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 311 | + .AnyAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 312 | + if (!ok) | |
| 313 | + { | |
| 314 | + throw new UserFriendlyException("所选合作伙伴不存在或已删除"); | |
| 315 | + } | |
| 316 | + } | |
| 317 | + | |
| 318 | + private async Task<string> ResolvePartnerNameAsync(string partnerId) | |
| 319 | + { | |
| 320 | + var p = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 321 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 322 | + if (p is null || string.IsNullOrWhiteSpace(p.PartnerName)) | |
| 323 | + { | |
| 324 | + return "无"; | |
| 325 | + } | |
| 326 | + | |
| 327 | + return p.PartnerName.Trim(); | |
| 328 | + } | |
| 329 | + | |
| 330 | + private static GroupGetOutputDto MapDetail(FlGroupDbEntity x, string partnerName) => new() | |
| 331 | + { | |
| 332 | + Id = x.Id, | |
| 333 | + GroupName = x.GroupName, | |
| 334 | + PartnerId = x.PartnerId, | |
| 335 | + PartnerName = partnerName, | |
| 336 | + State = x.State, | |
| 337 | + CreationTime = x.CreationTime, | |
| 338 | + LastModificationTime = x.LastModificationTime | |
| 339 | + }; | |
| 340 | + | |
| 341 | + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, | |
| 342 | + List<T> items) | |
| 343 | + { | |
| 344 | + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | |
| 345 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); | |
| 346 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | |
| 347 | + return new PagedResultWithPageDto<T> | |
| 348 | + { | |
| 349 | + PageIndex = pageIndex, | |
| 350 | + PageSize = pageSize, | |
| 351 | + TotalCount = total, | |
| 352 | + TotalPages = totalPages, | |
| 353 | + Items = items | |
| 354 | + }; | |
| 355 | + } | |
| 356 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Helpers; | |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 3 | +using FoodLabeling.Application.Contracts.Dtos.Partner; | |
| 4 | +using FoodLabeling.Application.Contracts.IServices; | |
| 5 | +using FoodLabeling.Application.Services.DbModels; | |
| 6 | +using Microsoft.AspNetCore.Mvc; | |
| 7 | +using QuestPDF.Fluent; | |
| 8 | +using QuestPDF.Helpers; | |
| 9 | +using QuestPDF.Infrastructure; | |
| 10 | +using SqlSugar; | |
| 11 | +using Volo.Abp; | |
| 12 | +using Volo.Abp.Application.Services; | |
| 13 | +using Volo.Abp.Guids; | |
| 14 | +using Volo.Abp.Uow; | |
| 15 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 16 | + | |
| 17 | +namespace FoodLabeling.Application.Services; | |
| 18 | + | |
| 19 | +/// <summary> | |
| 20 | +/// 合作伙伴管理(fl_partner) | |
| 21 | +/// </summary> | |
| 22 | +public class PartnerAppService : ApplicationService, IPartnerAppService | |
| 23 | +{ | |
| 24 | + private const int ExportPdfMaxRows = 5000; | |
| 25 | + | |
| 26 | + private readonly ISqlSugarDbContext _dbContext; | |
| 27 | + private readonly IGuidGenerator _guidGenerator; | |
| 28 | + | |
| 29 | + public PartnerAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | |
| 30 | + { | |
| 31 | + _dbContext = dbContext; | |
| 32 | + _guidGenerator = guidGenerator; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /// <inheritdoc /> | |
| 36 | + public async Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input) | |
| 37 | + { | |
| 38 | + RefAsync<int> total = 0; | |
| 39 | + var query = BuildPartnerListQuery(input); | |
| 40 | + | |
| 41 | + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | |
| 42 | + var items = entities.Select(MapListItem).ToList(); | |
| 43 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | |
| 44 | + } | |
| 45 | + | |
| 46 | + /// <inheritdoc /> | |
| 47 | + public async Task<PartnerGetOutputDto> GetAsync(string id) | |
| 48 | + { | |
| 49 | + var partnerId = id?.Trim(); | |
| 50 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 51 | + { | |
| 52 | + throw new UserFriendlyException("合作伙伴Id不能为空"); | |
| 53 | + } | |
| 54 | + | |
| 55 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 56 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 57 | + if (entity is null) | |
| 58 | + { | |
| 59 | + throw new UserFriendlyException("合作伙伴不存在"); | |
| 60 | + } | |
| 61 | + | |
| 62 | + return MapDetail(entity); | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <inheritdoc /> | |
| 66 | + [UnitOfWork] | |
| 67 | + public async Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input) | |
| 68 | + { | |
| 69 | + var name = input.PartnerName?.Trim(); | |
| 70 | + if (string.IsNullOrWhiteSpace(name)) | |
| 71 | + { | |
| 72 | + throw new UserFriendlyException("合作伙伴名称不能为空"); | |
| 73 | + } | |
| 74 | + | |
| 75 | + var email = input.ContactEmail?.Trim(); | |
| 76 | + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) | |
| 77 | + { | |
| 78 | + throw new UserFriendlyException("联系邮箱格式不正确"); | |
| 79 | + } | |
| 80 | + | |
| 81 | + var now = Clock.Now; | |
| 82 | + var entity = new FlPartnerDbEntity | |
| 83 | + { | |
| 84 | + Id = _guidGenerator.Create().ToString(), | |
| 85 | + IsDeleted = false, | |
| 86 | + PartnerName = name, | |
| 87 | + ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email, | |
| 88 | + PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(), | |
| 89 | + State = input.State, | |
| 90 | + CreationTime = now, | |
| 91 | + CreatorId = CurrentUser?.Id?.ToString(), | |
| 92 | + LastModificationTime = now, | |
| 93 | + LastModifierId = CurrentUser?.Id?.ToString() | |
| 94 | + }; | |
| 95 | + | |
| 96 | + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | |
| 97 | + return await GetAsync(entity.Id); | |
| 98 | + } | |
| 99 | + | |
| 100 | + /// <inheritdoc /> | |
| 101 | + [UnitOfWork] | |
| 102 | + public async Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input) | |
| 103 | + { | |
| 104 | + var partnerId = id?.Trim(); | |
| 105 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 106 | + { | |
| 107 | + throw new UserFriendlyException("合作伙伴Id不能为空"); | |
| 108 | + } | |
| 109 | + | |
| 110 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 111 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 112 | + if (entity is null) | |
| 113 | + { | |
| 114 | + throw new UserFriendlyException("合作伙伴不存在"); | |
| 115 | + } | |
| 116 | + | |
| 117 | + var name = input.PartnerName?.Trim(); | |
| 118 | + if (string.IsNullOrWhiteSpace(name)) | |
| 119 | + { | |
| 120 | + throw new UserFriendlyException("合作伙伴名称不能为空"); | |
| 121 | + } | |
| 122 | + | |
| 123 | + var email = input.ContactEmail?.Trim(); | |
| 124 | + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) | |
| 125 | + { | |
| 126 | + throw new UserFriendlyException("联系邮箱格式不正确"); | |
| 127 | + } | |
| 128 | + | |
| 129 | + entity.PartnerName = name; | |
| 130 | + entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email; | |
| 131 | + entity.PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(); | |
| 132 | + entity.State = input.State; | |
| 133 | + entity.LastModificationTime = Clock.Now; | |
| 134 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 135 | + | |
| 136 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 137 | + return await GetAsync(partnerId); | |
| 138 | + } | |
| 139 | + | |
| 140 | + /// <inheritdoc /> | |
| 141 | + [UnitOfWork] | |
| 142 | + public async Task DeleteAsync(string id) | |
| 143 | + { | |
| 144 | + var partnerId = id?.Trim(); | |
| 145 | + if (string.IsNullOrWhiteSpace(partnerId)) | |
| 146 | + { | |
| 147 | + throw new UserFriendlyException("合作伙伴Id不能为空"); | |
| 148 | + } | |
| 149 | + | |
| 150 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 151 | + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | |
| 152 | + if (entity is null) | |
| 153 | + { | |
| 154 | + throw new UserFriendlyException("合作伙伴不存在"); | |
| 155 | + } | |
| 156 | + | |
| 157 | + entity.IsDeleted = true; | |
| 158 | + entity.LastModificationTime = Clock.Now; | |
| 159 | + entity.LastModifierId = CurrentUser?.Id?.ToString(); | |
| 160 | + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | |
| 161 | + } | |
| 162 | + | |
| 163 | + /// <inheritdoc /> | |
| 164 | + public async Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input) | |
| 165 | + { | |
| 166 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 167 | + | |
| 168 | + var count = await BuildPartnerListQuery(input).CountAsync(); | |
| 169 | + var query = BuildPartnerListQuery(input); | |
| 170 | + if (count > ExportPdfMaxRows) | |
| 171 | + { | |
| 172 | + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | |
| 173 | + } | |
| 174 | + | |
| 175 | + var rows = await query.Take(ExportPdfMaxRows).ToListAsync(); | |
| 176 | + var fileName = $"partners_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 177 | + | |
| 178 | + var document = Document.Create(container => | |
| 179 | + { | |
| 180 | + container.Page(page => | |
| 181 | + { | |
| 182 | + page.Margin(28); | |
| 183 | + page.DefaultTextStyle(x => x.FontSize(10)); | |
| 184 | + page.Header().Text("Partners").SemiBold().FontSize(18); | |
| 185 | + page.Content().PaddingTop(12).Table(table => | |
| 186 | + { | |
| 187 | + table.ColumnsDefinition(c => | |
| 188 | + { | |
| 189 | + c.RelativeColumn(2.2f); | |
| 190 | + c.RelativeColumn(2.4f); | |
| 191 | + c.RelativeColumn(1.8f); | |
| 192 | + c.RelativeColumn(1f); | |
| 193 | + c.RelativeColumn(1.6f); | |
| 194 | + }); | |
| 195 | + | |
| 196 | + static IContainer CellHeader(IContainer c) => | |
| 197 | + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold()); | |
| 198 | + | |
| 199 | + table.Cell().Element(CellHeader).Text("Partner"); | |
| 200 | + table.Cell().Element(CellHeader).Text("Contact"); | |
| 201 | + table.Cell().Element(CellHeader).Text("Phone"); | |
| 202 | + table.Cell().Element(CellHeader).Text("Status"); | |
| 203 | + table.Cell().Element(CellHeader).Text("Created"); | |
| 204 | + | |
| 205 | + foreach (var e in rows) | |
| 206 | + { | |
| 207 | + var status = e.State ? "active" : "inactive"; | |
| 208 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 209 | + .Text(e.PartnerName ?? string.Empty); | |
| 210 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 211 | + .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "无" : e.ContactEmail!); | |
| 212 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 213 | + .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "无" : e.PhoneNumber!); | |
| 214 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); | |
| 215 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | |
| 216 | + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); | |
| 217 | + } | |
| 218 | + }); | |
| 219 | + }); | |
| 220 | + }); | |
| 221 | + | |
| 222 | + var stream = new MemoryStream(); | |
| 223 | + document.GeneratePdf(stream); | |
| 224 | + stream.Position = 0; | |
| 225 | + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; | |
| 226 | + } | |
| 227 | + | |
| 228 | + private ISugarQueryable<FlPartnerDbEntity> BuildPartnerListQuery(PartnerGetListInputVo input) | |
| 229 | + { | |
| 230 | + var keyword = input.Keyword?.Trim(); | |
| 231 | + var query = _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 232 | + .Where(x => !x.IsDeleted) | |
| 233 | + .WhereIF(input.State != null, x => x.State == input.State) | |
| 234 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), | |
| 235 | + x => x.PartnerName.Contains(keyword!) || | |
| 236 | + (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) || | |
| 237 | + (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!))); | |
| 238 | + | |
| 239 | + if (!string.IsNullOrWhiteSpace(input.Sorting)) | |
| 240 | + { | |
| 241 | + var sorting = input.Sorting.Trim(); | |
| 242 | + if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase)) | |
| 243 | + { | |
| 244 | + query = query.OrderByDescending(x => x.PartnerName); | |
| 245 | + } | |
| 246 | + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase)) | |
| 247 | + { | |
| 248 | + query = query.OrderBy(x => x.PartnerName); | |
| 249 | + } | |
| 250 | + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) | |
| 251 | + { | |
| 252 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 253 | + } | |
| 254 | + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) | |
| 255 | + { | |
| 256 | + query = query.OrderBy(x => x.CreationTime); | |
| 257 | + } | |
| 258 | + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase)) | |
| 259 | + { | |
| 260 | + query = query.OrderByDescending(x => x.State); | |
| 261 | + } | |
| 262 | + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase)) | |
| 263 | + { | |
| 264 | + query = query.OrderBy(x => x.State); | |
| 265 | + } | |
| 266 | + else | |
| 267 | + { | |
| 268 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 269 | + } | |
| 270 | + } | |
| 271 | + else | |
| 272 | + { | |
| 273 | + query = query.OrderByDescending(x => x.CreationTime); | |
| 274 | + } | |
| 275 | + | |
| 276 | + return query; | |
| 277 | + } | |
| 278 | + | |
| 279 | + private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) => new() | |
| 280 | + { | |
| 281 | + Id = x.Id, | |
| 282 | + PartnerName = x.PartnerName, | |
| 283 | + ContactEmail = x.ContactEmail, | |
| 284 | + PhoneNumber = x.PhoneNumber, | |
| 285 | + State = x.State, | |
| 286 | + CreationTime = x.CreationTime | |
| 287 | + }; | |
| 288 | + | |
| 289 | + private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) => new() | |
| 290 | + { | |
| 291 | + Id = x.Id, | |
| 292 | + PartnerName = x.PartnerName, | |
| 293 | + ContactEmail = x.ContactEmail, | |
| 294 | + PhoneNumber = x.PhoneNumber, | |
| 295 | + State = x.State, | |
| 296 | + CreationTime = x.CreationTime, | |
| 297 | + LastModificationTime = x.LastModificationTime | |
| 298 | + }; | |
| 299 | + | |
| 300 | + private static bool IsPlausibleEmail(string email) | |
| 301 | + { | |
| 302 | + if (email.Length > 256) | |
| 303 | + { | |
| 304 | + return false; | |
| 305 | + } | |
| 306 | + | |
| 307 | + var at = email.IndexOf('@'); | |
| 308 | + return at > 0 && at < email.Length - 1 && email.IndexOf('@', at + 1) < 0; | |
| 309 | + } | |
| 310 | + | |
| 311 | + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, | |
| 312 | + List<T> items) | |
| 313 | + { | |
| 314 | + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | |
| 315 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); | |
| 316 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | |
| 317 | + return new PagedResultWithPageDto<T> | |
| 318 | + { | |
| 319 | + PageIndex = pageIndex, | |
| 320 | + PageSize = pageSize, | |
| 321 | + TotalCount = total, | |
| 322 | + TotalPages = totalPages, | |
| 323 | + Items = items | |
| 324 | + }; | |
| 325 | + } | |
| 326 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs
0 → 100644
| 1 | +using System.Globalization; | |
| 2 | +using System.Text.Json; | |
| 3 | +using FoodLabeling.Application.Helpers; | |
| 4 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 5 | +using FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 6 | +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | |
| 7 | +using FoodLabeling.Application.Contracts.IServices; | |
| 8 | +using FoodLabeling.Application.Services.DbModels; | |
| 9 | +using FoodLabeling.Domain.Entities; | |
| 10 | +using Microsoft.AspNetCore.Authorization; | |
| 11 | +using Microsoft.AspNetCore.Mvc; | |
| 12 | +using QuestPDF.Fluent; | |
| 13 | +using QuestPDF.Helpers; | |
| 14 | +using QuestPDF.Infrastructure; | |
| 15 | +using SqlSugar; | |
| 16 | +using Volo.Abp; | |
| 17 | +using Volo.Abp.Application.Services; | |
| 18 | +using Yi.Framework.Rbac.Domain.Entities; | |
| 19 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 20 | + | |
| 21 | +namespace FoodLabeling.Application.Services; | |
| 22 | + | |
| 23 | +/// <summary> | |
| 24 | +/// Reports(Print Log / Label Report) | |
| 25 | +/// </summary> | |
| 26 | +[Authorize] | |
| 27 | +public class ReportsAppService : ApplicationService, IReportsAppService | |
| 28 | +{ | |
| 29 | + private const int ExportPdfMaxRows = 5000; | |
| 30 | + | |
| 31 | + private readonly ISqlSugarDbContext _dbContext; | |
| 32 | + private readonly IUsAppLabelingAppService _usAppLabelingAppService; | |
| 33 | + | |
| 34 | + public ReportsAppService(ISqlSugarDbContext dbContext, IUsAppLabelingAppService usAppLabelingAppService) | |
| 35 | + { | |
| 36 | + _dbContext = dbContext; | |
| 37 | + _usAppLabelingAppService = usAppLabelingAppService; | |
| 38 | + } | |
| 39 | + | |
| 40 | + /// <inheritdoc /> | |
| 41 | + public async Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync( | |
| 42 | + ReportsPrintLogGetListInputVo input) | |
| 43 | + { | |
| 44 | + if (input is null) | |
| 45 | + { | |
| 46 | + throw new UserFriendlyException("入参不能为空"); | |
| 47 | + } | |
| 48 | + | |
| 49 | + if (!CurrentUser.Id.HasValue) | |
| 50 | + { | |
| 51 | + throw new UserFriendlyException("用户未登录"); | |
| 52 | + } | |
| 53 | + | |
| 54 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 55 | + if (locationIds is not null && locationIds.Count == 0) | |
| 56 | + { | |
| 57 | + return EmptyPrintLogPage(input); | |
| 58 | + } | |
| 59 | + | |
| 60 | + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 61 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 62 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 63 | + var keyword = input.Keyword?.Trim(); | |
| 64 | + | |
| 65 | + RefAsync<int> total = 0; | |
| 66 | + | |
| 67 | + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 68 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) | |
| 69 | + .Where((t, l, p, lc, pc, loc, tpl) => | |
| 70 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && | |
| 71 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl); | |
| 72 | + | |
| 73 | + if (!string.IsNullOrWhiteSpace(input.Sorting) && | |
| 74 | + input.Sorting.Trim().Equals("PrintedAt asc", StringComparison.OrdinalIgnoreCase)) | |
| 75 | + { | |
| 76 | + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 77 | + OrderByType.Asc); | |
| 78 | + } | |
| 79 | + else | |
| 80 | + { | |
| 81 | + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 82 | + OrderByType.Desc); | |
| 83 | + } | |
| 84 | + | |
| 85 | + var pageRows = await query | |
| 86 | + .Select((t, l, p, lc, pc, loc, tpl) => new | |
| 87 | + { | |
| 88 | + t.Id, | |
| 89 | + LabelCode = l.LabelCode, | |
| 90 | + ProductName = p.ProductName, | |
| 91 | + LabelCategoryName = lc.CategoryName, | |
| 92 | + ProductCategoryName = pc.CategoryName, | |
| 93 | + tpl.Width, | |
| 94 | + tpl.Height, | |
| 95 | + tpl.Unit, | |
| 96 | + tpl.TemplateName, | |
| 97 | + t.PrintInputJson, | |
| 98 | + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 99 | + t.CreatedBy, | |
| 100 | + t.LocationId, | |
| 101 | + LocName = loc.LocationName, | |
| 102 | + LocCode = loc.LocationCode | |
| 103 | + }) | |
| 104 | + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | |
| 105 | + | |
| 106 | + var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 107 | + .Select(x => x!).Distinct().ToList()); | |
| 108 | + | |
| 109 | + var items = pageRows.Select(x => | |
| 110 | + { | |
| 111 | + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) | |
| 112 | + ? x.ProductCategoryName!.Trim() | |
| 113 | + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); | |
| 114 | + var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); | |
| 115 | + var locText = FormatLocationText(x.LocName, x.LocCode); | |
| 116 | + var printedAt = x.PrintedAt ?? DateTime.MinValue; | |
| 117 | + return new ReportsPrintLogListItemDto | |
| 118 | + { | |
| 119 | + TaskId = x.Id, | |
| 120 | + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), | |
| 121 | + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), | |
| 122 | + CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, | |
| 123 | + TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, | |
| 124 | + PrintedAt = printedAt, | |
| 125 | + PrintedByName = ResolveUserName(userMap, x.CreatedBy), | |
| 126 | + LocationText = locText, | |
| 127 | + LocationId = x.LocationId?.Trim(), | |
| 128 | + ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) | |
| 129 | + }; | |
| 130 | + }).ToList(); | |
| 131 | + | |
| 132 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /// <inheritdoc /> | |
| 136 | + public async Task<IActionResult> ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input) | |
| 137 | + { | |
| 138 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 139 | + if (input is null) | |
| 140 | + { | |
| 141 | + throw new UserFriendlyException("入参不能为空"); | |
| 142 | + } | |
| 143 | + | |
| 144 | + if (!CurrentUser.Id.HasValue) | |
| 145 | + { | |
| 146 | + throw new UserFriendlyException("用户未登录"); | |
| 147 | + } | |
| 148 | + | |
| 149 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 150 | + if (locationIds is not null && locationIds.Count == 0) | |
| 151 | + { | |
| 152 | + return BuildEmptyPdf("print-log-empty.pdf"); | |
| 153 | + } | |
| 154 | + | |
| 155 | + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 156 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 157 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 158 | + var keyword = input.Keyword?.Trim(); | |
| 159 | + | |
| 160 | + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 161 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) | |
| 162 | + .Where((t, l, p, lc, pc, loc, tpl) => | |
| 163 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && | |
| 164 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) | |
| 165 | + .OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc); | |
| 166 | + | |
| 167 | + var count = await query.CountAsync(); | |
| 168 | + if (count > ExportPdfMaxRows) | |
| 169 | + { | |
| 170 | + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | |
| 171 | + } | |
| 172 | + | |
| 173 | + var rows = await query.Take(ExportPdfMaxRows) | |
| 174 | + .Select((t, l, p, lc, pc, loc, tpl) => new | |
| 175 | + { | |
| 176 | + t.Id, | |
| 177 | + LabelCode = l.LabelCode, | |
| 178 | + ProductName = p.ProductName, | |
| 179 | + LabelCategoryName = lc.CategoryName, | |
| 180 | + ProductCategoryName = pc.CategoryName, | |
| 181 | + tpl.Width, | |
| 182 | + tpl.Height, | |
| 183 | + tpl.Unit, | |
| 184 | + tpl.TemplateName, | |
| 185 | + t.PrintInputJson, | |
| 186 | + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), | |
| 187 | + t.CreatedBy, | |
| 188 | + LocName = loc.LocationName, | |
| 189 | + LocCode = loc.LocationCode | |
| 190 | + }) | |
| 191 | + .ToListAsync(); | |
| 192 | + | |
| 193 | + var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 194 | + .Select(x => x!).Distinct().ToList()); | |
| 195 | + | |
| 196 | + var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 197 | + var document = Document.Create(container => | |
| 198 | + { | |
| 199 | + container.Page(page => | |
| 200 | + { | |
| 201 | + page.Margin(22); | |
| 202 | + page.DefaultTextStyle(x => x.FontSize(8.5f)); | |
| 203 | + page.Header().Text("Print Log").SemiBold().FontSize(16); | |
| 204 | + page.Content().PaddingTop(10).Table(table => | |
| 205 | + { | |
| 206 | + table.ColumnsDefinition(c => | |
| 207 | + { | |
| 208 | + c.RelativeColumn(1.1f); | |
| 209 | + c.RelativeColumn(1.2f); | |
| 210 | + c.RelativeColumn(0.9f); | |
| 211 | + c.RelativeColumn(1.1f); | |
| 212 | + c.RelativeColumn(1f); | |
| 213 | + c.RelativeColumn(0.9f); | |
| 214 | + c.RelativeColumn(0.9f); | |
| 215 | + c.RelativeColumn(0.8f); | |
| 216 | + }); | |
| 217 | + static IContainer H(IContainer x) => | |
| 218 | + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); | |
| 219 | + table.Cell().Element(H).Text("Label ID"); | |
| 220 | + table.Cell().Element(H).Text("Product"); | |
| 221 | + table.Cell().Element(H).Text("Category"); | |
| 222 | + table.Cell().Element(H).Text("Template"); | |
| 223 | + table.Cell().Element(H).Text("Printed At"); | |
| 224 | + table.Cell().Element(H).Text("Printed By"); | |
| 225 | + table.Cell().Element(H).Text("Location"); | |
| 226 | + table.Cell().Element(H).Text("Expiry"); | |
| 227 | + | |
| 228 | + foreach (var x in rows) | |
| 229 | + { | |
| 230 | + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) | |
| 231 | + ? x.ProductCategoryName!.Trim() | |
| 232 | + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); | |
| 233 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 234 | + .Text(string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim()); | |
| 235 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 236 | + .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); | |
| 237 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); | |
| 238 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 239 | + .Text(FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName)); | |
| 240 | + var printedAt = x.PrintedAt ?? DateTime.MinValue; | |
| 241 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 242 | + .Text(printedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); | |
| 243 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 244 | + .Text(ResolveUserName(userMap, x.CreatedBy)); | |
| 245 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 246 | + .Text(FormatLocationText(x.LocName, x.LocCode)); | |
| 247 | + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) | |
| 248 | + .Text(TryExtractExpiryText(x.PrintInputJson)); | |
| 249 | + } | |
| 250 | + }); | |
| 251 | + }); | |
| 252 | + }); | |
| 253 | + | |
| 254 | + var ms = new MemoryStream(); | |
| 255 | + document.GeneratePdf(ms); | |
| 256 | + ms.Position = 0; | |
| 257 | + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; | |
| 258 | + } | |
| 259 | + | |
| 260 | + /// <inheritdoc /> | |
| 261 | + public Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input) => | |
| 262 | + _usAppLabelingAppService.ReprintAsync(input); | |
| 263 | + | |
| 264 | + /// <inheritdoc /> | |
| 265 | + public async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input) | |
| 266 | + { | |
| 267 | + if (input is null) | |
| 268 | + { | |
| 269 | + throw new UserFriendlyException("入参不能为空"); | |
| 270 | + } | |
| 271 | + | |
| 272 | + if (!CurrentUser.Id.HasValue) | |
| 273 | + { | |
| 274 | + throw new UserFriendlyException("用户未登录"); | |
| 275 | + } | |
| 276 | + | |
| 277 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 278 | + if (locationIds is not null && locationIds.Count == 0) | |
| 279 | + { | |
| 280 | + return new ReportsLabelReportOutputDto(); | |
| 281 | + } | |
| 282 | + | |
| 283 | + var (curStart, curEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 284 | + var span = curEndExcl - curStart; | |
| 285 | + if (span.TotalDays < 1) | |
| 286 | + { | |
| 287 | + span = TimeSpan.FromDays(1); | |
| 288 | + } | |
| 289 | + | |
| 290 | + var prevEndExcl = curStart; | |
| 291 | + var prevStart = curStart - span; | |
| 292 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 293 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 294 | + var keyword = input.Keyword?.Trim(); | |
| 295 | + | |
| 296 | + var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 297 | + .Where((t, l, p, lc, pc, loc) => | |
| 298 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 299 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 300 | + .CountAsync(); | |
| 301 | + | |
| 302 | + var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 303 | + .Where((t, l, p, lc, pc, loc) => | |
| 304 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && | |
| 305 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) | |
| 306 | + .CountAsync(); | |
| 307 | + | |
| 308 | + var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays)); | |
| 309 | + var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays)); | |
| 310 | + var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); | |
| 311 | + var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); | |
| 312 | + | |
| 313 | + var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 314 | + .Where((t, l, p, lc, pc, loc) => | |
| 315 | + l.LabelCategoryId != null && | |
| 316 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 317 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 318 | + .GroupBy((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName }) | |
| 319 | + .Select((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) | |
| 320 | + .ToListAsync(); | |
| 321 | + | |
| 322 | + var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); | |
| 323 | + | |
| 324 | + var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 325 | + .Where((t, l, p, lc, pc, loc) => | |
| 326 | + !string.IsNullOrEmpty(p.Id) && | |
| 327 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 328 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 329 | + .GroupBy((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, Cat = pc.CategoryName }) | |
| 330 | + .Select((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, CategoryName = pc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) | |
| 331 | + .ToListAsync(); | |
| 332 | + | |
| 333 | + var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); | |
| 334 | + var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList(); | |
| 335 | + | |
| 336 | + var trendEndDay = curEndExcl.Date.AddDays(-1); | |
| 337 | + var trendStartDay = trendEndDay.AddDays(-6); | |
| 338 | + if (trendStartDay < curStart.Date) | |
| 339 | + { | |
| 340 | + trendStartDay = curStart.Date; | |
| 341 | + } | |
| 342 | + | |
| 343 | + var trendEndExcl = trendEndDay.AddDays(1); | |
| 344 | + | |
| 345 | + var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 346 | + .Where((t, l, p, lc, pc, loc) => | |
| 347 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && | |
| 348 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) | |
| 349 | + .Select((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime)) | |
| 350 | + .ToListAsync(); | |
| 351 | + | |
| 352 | + var trendDict = trendRaw | |
| 353 | + .Where(x => x.HasValue) | |
| 354 | + .GroupBy(x => x!.Value.Date) | |
| 355 | + .ToDictionary(g => g.Key, g => g.Count()); | |
| 356 | + | |
| 357 | + var trend = new List<ReportsDailyCountDto>(); | |
| 358 | + for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1)) | |
| 359 | + { | |
| 360 | + trend.Add(new ReportsDailyCountDto | |
| 361 | + { | |
| 362 | + Date = d.ToString("yyyy-MM-dd"), | |
| 363 | + Count = trendDict.TryGetValue(d, out var c) ? c : 0 | |
| 364 | + }); | |
| 365 | + } | |
| 366 | + | |
| 367 | + var byCategory = categoryRows | |
| 368 | + .OrderByDescending(x => x.Cnt) | |
| 369 | + .Select(x => new ReportsCategoryCountDto | |
| 370 | + { | |
| 371 | + CategoryId = x.Id ?? string.Empty, | |
| 372 | + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? "无" : x.CategoryName.Trim(), | |
| 373 | + Count = x.Cnt | |
| 374 | + }) | |
| 375 | + .ToList(); | |
| 376 | + | |
| 377 | + var mostUsed = topList.Select(x => | |
| 378 | + { | |
| 379 | + var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2); | |
| 380 | + return new ReportsTopProductRowDto | |
| 381 | + { | |
| 382 | + ProductId = x.Id ?? string.Empty, | |
| 383 | + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), | |
| 384 | + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? "无" : x.CategoryName!.Trim(), | |
| 385 | + TotalPrinted = x.Cnt, | |
| 386 | + UsagePercent = pct | |
| 387 | + }; | |
| 388 | + }).ToList(); | |
| 389 | + | |
| 390 | + return new ReportsLabelReportOutputDto | |
| 391 | + { | |
| 392 | + Summary = new ReportsLabelReportSummaryDto | |
| 393 | + { | |
| 394 | + TotalLabelsPrinted = totalCur, | |
| 395 | + TotalLabelsPrintedPrevPeriod = totalPrev, | |
| 396 | + TotalLabelsPrintedChangeRate = CalcChangeRate(totalCur, totalPrev), | |
| 397 | + MostPrintedCategoryName = topCat is null ? "无" : (topCat.CategoryName?.Trim() ?? "无"), | |
| 398 | + MostPrintedCategoryCount = topCat?.Cnt ?? 0, | |
| 399 | + TopProductName = topProd is null ? "无" : (topProd.ProductName?.Trim() ?? "无"), | |
| 400 | + TopProductCount = topProd?.Cnt ?? 0, | |
| 401 | + AvgDailyPrints = avgDaily, | |
| 402 | + AvgDailyPrintsPrevPeriod = avgDailyPrev, | |
| 403 | + AvgDailyPrintsChangeRate = CalcChangeRate(avgDaily, avgDailyPrev) | |
| 404 | + }, | |
| 405 | + LabelsByCategory = byCategory, | |
| 406 | + PrintVolumeTrend = trend, | |
| 407 | + MostUsedProducts = mostUsed | |
| 408 | + }; | |
| 409 | + } | |
| 410 | + | |
| 411 | + /// <inheritdoc /> | |
| 412 | + public async Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input) | |
| 413 | + { | |
| 414 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 415 | + var data = await GetLabelReportAsync(input); | |
| 416 | + var fileName = $"label-report_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; | |
| 417 | + var document = Document.Create(container => | |
| 418 | + { | |
| 419 | + container.Page(page => | |
| 420 | + { | |
| 421 | + page.Margin(24); | |
| 422 | + page.DefaultTextStyle(x => x.FontSize(9)); | |
| 423 | + page.Header().Text("Label Report").SemiBold().FontSize(16); | |
| 424 | + page.Content().Column(col => | |
| 425 | + { | |
| 426 | + col.Spacing(10); | |
| 427 | + col.Item().Text( | |
| 428 | + $"Total printed: {data.Summary.TotalLabelsPrinted} (prev: {data.Summary.TotalLabelsPrintedPrevPeriod}, Δ%: {data.Summary.TotalLabelsPrintedChangeRate:0.##}%)"); | |
| 429 | + col.Item().Text( | |
| 430 | + $"Top category: {data.Summary.MostPrintedCategoryName} ({data.Summary.MostPrintedCategoryCount})"); | |
| 431 | + col.Item().Text($"Top product: {data.Summary.TopProductName} ({data.Summary.TopProductCount})"); | |
| 432 | + col.Item().Text( | |
| 433 | + $"Avg daily: {data.Summary.AvgDailyPrints:0.##} (prev: {data.Summary.AvgDailyPrintsPrevPeriod:0.##}, Δ%: {data.Summary.AvgDailyPrintsChangeRate:0.##}%)"); | |
| 434 | + col.Item().Text("By category:").SemiBold(); | |
| 435 | + col.Item().Table(t => | |
| 436 | + { | |
| 437 | + t.ColumnsDefinition(c => { c.RelativeColumn(2); c.RelativeColumn(1); }); | |
| 438 | + t.Cell().Element(HeaderCell).Text("Category"); | |
| 439 | + t.Cell().Element(HeaderCell).Text("Count"); | |
| 440 | + foreach (var r in data.LabelsByCategory) | |
| 441 | + { | |
| 442 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); | |
| 443 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); | |
| 444 | + } | |
| 445 | + }); | |
| 446 | + col.Item().Text("Daily trend:").SemiBold(); | |
| 447 | + col.Item().Table(t => | |
| 448 | + { | |
| 449 | + t.ColumnsDefinition(c => { c.RelativeColumn(1.2f); c.RelativeColumn(1); }); | |
| 450 | + t.Cell().Element(HeaderCell).Text("Date"); | |
| 451 | + t.Cell().Element(HeaderCell).Text("Count"); | |
| 452 | + foreach (var r in data.PrintVolumeTrend) | |
| 453 | + { | |
| 454 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Date); | |
| 455 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); | |
| 456 | + } | |
| 457 | + }); | |
| 458 | + col.Item().Text("Most used products:").SemiBold(); | |
| 459 | + col.Item().Table(t => | |
| 460 | + { | |
| 461 | + t.ColumnsDefinition(c => | |
| 462 | + { | |
| 463 | + c.RelativeColumn(1.5f); | |
| 464 | + c.RelativeColumn(1f); | |
| 465 | + c.RelativeColumn(0.8f); | |
| 466 | + c.RelativeColumn(0.7f); | |
| 467 | + }); | |
| 468 | + t.Cell().Element(HeaderCell).Text("Product"); | |
| 469 | + t.Cell().Element(HeaderCell).Text("Category"); | |
| 470 | + t.Cell().Element(HeaderCell).Text("Total"); | |
| 471 | + t.Cell().Element(HeaderCell).Text("%"); | |
| 472 | + foreach (var r in data.MostUsedProducts) | |
| 473 | + { | |
| 474 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.ProductName); | |
| 475 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); | |
| 476 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.TotalPrinted.ToString()); | |
| 477 | + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.UsagePercent.ToString("0.##")); | |
| 478 | + } | |
| 479 | + }); | |
| 480 | + }); | |
| 481 | + }); | |
| 482 | + }); | |
| 483 | + | |
| 484 | + static IContainer HeaderCell(IContainer x) => | |
| 485 | + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); | |
| 486 | + | |
| 487 | + var ms = new MemoryStream(); | |
| 488 | + document.GeneratePdf(ms); | |
| 489 | + ms.Position = 0; | |
| 490 | + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; | |
| 491 | + } | |
| 492 | + | |
| 493 | + private ISugarQueryable<FlLabelPrintTaskDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, | |
| 494 | + FlProductCategoryDbEntity, LocationAggregateRoot> | |
| 495 | + BuildReportTaskCore( | |
| 496 | + List<string>? locationIds, | |
| 497 | + bool isAdmin, | |
| 498 | + string currentUserIdStr, | |
| 499 | + string? keyword) | |
| 500 | + { | |
| 501 | + return _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 502 | + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) | |
| 503 | + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id) | |
| 504 | + .LeftJoin<FlLabelCategoryDbEntity>((t, l, p, lc) => l.LabelCategoryId == lc.Id) | |
| 505 | + .LeftJoin<FlProductCategoryDbEntity>((t, l, p, lc, pc) => p.CategoryId == pc.Id) | |
| 506 | + .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) => | |
| 507 | + t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) | |
| 508 | + .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) | |
| 509 | + .WhereIF(!isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) | |
| 510 | + .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) | |
| 511 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), | |
| 512 | + (t, l, p, lc, pc, loc) => | |
| 513 | + (p.ProductName != null && p.ProductName.Contains(keyword!)) || | |
| 514 | + (lc.CategoryName != null && lc.CategoryName.Contains(keyword!)) || | |
| 515 | + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!))); | |
| 516 | + } | |
| 517 | + | |
| 518 | + private static decimal CalcChangeRate(decimal current, decimal previous) | |
| 519 | + { | |
| 520 | + if (previous == 0) | |
| 521 | + { | |
| 522 | + return current > 0 ? 100m : 0m; | |
| 523 | + } | |
| 524 | + | |
| 525 | + return Math.Round((current - previous) * 100m / previous, 2); | |
| 526 | + } | |
| 527 | + | |
| 528 | + private static decimal CalcChangeRate(int current, int previous) => | |
| 529 | + CalcChangeRate((decimal)current, (decimal)previous); | |
| 530 | + | |
| 531 | + private async Task<List<string>?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, | |
| 532 | + string? locationId) | |
| 533 | + { | |
| 534 | + var locId = locationId?.Trim(); | |
| 535 | + if (!string.IsNullOrWhiteSpace(locId)) | |
| 536 | + { | |
| 537 | + return new List<string> { locId }; | |
| 538 | + } | |
| 539 | + | |
| 540 | + var gid = groupId?.Trim(); | |
| 541 | + var pid = partnerId?.Trim(); | |
| 542 | + | |
| 543 | + if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) | |
| 544 | + { | |
| 545 | + return null; | |
| 546 | + } | |
| 547 | + | |
| 548 | + var q = _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>().Where(x => !x.IsDeleted); | |
| 549 | + | |
| 550 | + if (!string.IsNullOrWhiteSpace(gid)) | |
| 551 | + { | |
| 552 | + var g = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 553 | + .FirstAsync(x => !x.IsDeleted && x.Id == gid); | |
| 554 | + if (g is null) | |
| 555 | + { | |
| 556 | + return new List<string>(); | |
| 557 | + } | |
| 558 | + | |
| 559 | + var gName = g.GroupName?.Trim() ?? string.Empty; | |
| 560 | + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 561 | + .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); | |
| 562 | + var pName = partner?.PartnerName?.Trim() ?? string.Empty; | |
| 563 | + q = q.Where(x => x.GroupName == gName && x.Partner == pName); | |
| 564 | + } | |
| 565 | + else if (!string.IsNullOrWhiteSpace(pid)) | |
| 566 | + { | |
| 567 | + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 568 | + .FirstAsync(x => !x.IsDeleted && x.Id == pid); | |
| 569 | + if (partner is null) | |
| 570 | + { | |
| 571 | + return new List<string>(); | |
| 572 | + } | |
| 573 | + | |
| 574 | + var pName = partner.PartnerName?.Trim() ?? string.Empty; | |
| 575 | + q = q.Where(x => x.Partner == pName); | |
| 576 | + } | |
| 577 | + | |
| 578 | + var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); | |
| 579 | + return ids; | |
| 580 | + } | |
| 581 | + | |
| 582 | + private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, | |
| 583 | + DateTime? endDate) | |
| 584 | + { | |
| 585 | + var endDay = (endDate ?? DateTime.Today).Date; | |
| 586 | + var endExcl = endDay.AddDays(1); | |
| 587 | + var start = (startDate ?? endDay.AddDays(-29)).Date; | |
| 588 | + if (start >= endExcl) | |
| 589 | + { | |
| 590 | + start = endExcl.AddDays(-1); | |
| 591 | + } | |
| 592 | + | |
| 593 | + return (start, endExcl); | |
| 594 | + } | |
| 595 | + | |
| 596 | + private static PagedResultWithPageDto<ReportsPrintLogListItemDto> EmptyPrintLogPage( | |
| 597 | + ReportsPrintLogGetListInputVo input) | |
| 598 | + { | |
| 599 | + var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; | |
| 600 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); | |
| 601 | + return new PagedResultWithPageDto<ReportsPrintLogListItemDto> | |
| 602 | + { | |
| 603 | + PageIndex = pageIndex, | |
| 604 | + PageSize = pageSize, | |
| 605 | + TotalCount = 0, | |
| 606 | + TotalPages = 0, | |
| 607 | + Items = new List<ReportsPrintLogListItemDto>() | |
| 608 | + }; | |
| 609 | + } | |
| 610 | + | |
| 611 | + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, | |
| 612 | + List<T> items) | |
| 613 | + { | |
| 614 | + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | |
| 615 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); | |
| 616 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | |
| 617 | + return new PagedResultWithPageDto<T> | |
| 618 | + { | |
| 619 | + PageIndex = pageIndex, | |
| 620 | + PageSize = pageSize, | |
| 621 | + TotalCount = total, | |
| 622 | + TotalPages = totalPages, | |
| 623 | + Items = items | |
| 624 | + }; | |
| 625 | + } | |
| 626 | + | |
| 627 | + private async Task<Dictionary<string, string>> LoadUserNameMapAsync(List<string> userIdStrings) | |
| 628 | + { | |
| 629 | + var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| 630 | + if (userIdStrings.Count == 0) | |
| 631 | + { | |
| 632 | + return map; | |
| 633 | + } | |
| 634 | + | |
| 635 | + var guids = userIdStrings | |
| 636 | + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) | |
| 637 | + .Where(x => x.HasValue) | |
| 638 | + .Select(x => x!.Value) | |
| 639 | + .Distinct() | |
| 640 | + .ToList(); | |
| 641 | + if (guids.Count == 0) | |
| 642 | + { | |
| 643 | + return map; | |
| 644 | + } | |
| 645 | + | |
| 646 | + var users = await _dbContext.SqlSugarClient.Queryable<UserAggregateRoot>() | |
| 647 | + .Where(u => !u.IsDeleted && guids.Contains(u.Id)) | |
| 648 | + .Select(u => new { u.Id, u.Name, u.UserName }) | |
| 649 | + .ToListAsync(); | |
| 650 | + | |
| 651 | + foreach (var u in users) | |
| 652 | + { | |
| 653 | + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); | |
| 654 | + map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; | |
| 655 | + } | |
| 656 | + | |
| 657 | + return map; | |
| 658 | + } | |
| 659 | + | |
| 660 | + private static string ResolveUserName(Dictionary<string, string> map, string? createdBy) | |
| 661 | + { | |
| 662 | + if (string.IsNullOrWhiteSpace(createdBy)) | |
| 663 | + { | |
| 664 | + return "无"; | |
| 665 | + } | |
| 666 | + | |
| 667 | + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; | |
| 668 | + } | |
| 669 | + | |
| 670 | + private static string FormatLocationText(string? locName, string? locCode) | |
| 671 | + { | |
| 672 | + var n = locName?.Trim(); | |
| 673 | + var c = locCode?.Trim(); | |
| 674 | + if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(c)) | |
| 675 | + { | |
| 676 | + return "无"; | |
| 677 | + } | |
| 678 | + | |
| 679 | + if (string.IsNullOrWhiteSpace(c)) | |
| 680 | + { | |
| 681 | + return n ?? "无"; | |
| 682 | + } | |
| 683 | + | |
| 684 | + if (string.IsNullOrWhiteSpace(n)) | |
| 685 | + { | |
| 686 | + return $"({c})"; | |
| 687 | + } | |
| 688 | + | |
| 689 | + return $"{n} ({c})"; | |
| 690 | + } | |
| 691 | + | |
| 692 | + private static string FormatTemplateDisplay(decimal w, decimal h, string? unit, string? templateName) | |
| 693 | + { | |
| 694 | + var size = FormatLabelSizeWithUnit(w, h, unit ?? "inch"); | |
| 695 | + var tn = templateName?.Trim(); | |
| 696 | + if (string.IsNullOrWhiteSpace(tn)) | |
| 697 | + { | |
| 698 | + return size ?? "无"; | |
| 699 | + } | |
| 700 | + | |
| 701 | + return string.IsNullOrWhiteSpace(size) ? tn : $"{size} {tn}"; | |
| 702 | + } | |
| 703 | + | |
| 704 | + private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit) | |
| 705 | + { | |
| 706 | + var u = (unit ?? "inch").Trim().ToLowerInvariant(); | |
| 707 | + var ws = w.ToString(CultureInfo.InvariantCulture); | |
| 708 | + var hs = h.ToString(CultureInfo.InvariantCulture); | |
| 709 | + var normalizedUnit = u is "in" ? "inch" : u; | |
| 710 | + return $"{ws}x{hs}{normalizedUnit}"; | |
| 711 | + } | |
| 712 | + | |
| 713 | + private static string TryExtractExpiryText(string? printInputJson) | |
| 714 | + { | |
| 715 | + if (string.IsNullOrWhiteSpace(printInputJson)) | |
| 716 | + { | |
| 717 | + return "无"; | |
| 718 | + } | |
| 719 | + | |
| 720 | + try | |
| 721 | + { | |
| 722 | + using var doc = JsonDocument.Parse(printInputJson); | |
| 723 | + if (doc.RootElement.ValueKind != JsonValueKind.Object) | |
| 724 | + { | |
| 725 | + return "无"; | |
| 726 | + } | |
| 727 | + | |
| 728 | + foreach (var prop in doc.RootElement.EnumerateObject()) | |
| 729 | + { | |
| 730 | + var key = prop.Name.Trim(); | |
| 731 | + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && | |
| 732 | + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && | |
| 733 | + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) | |
| 734 | + { | |
| 735 | + continue; | |
| 736 | + } | |
| 737 | + | |
| 738 | + var v = prop.Value; | |
| 739 | + if (v.ValueKind == JsonValueKind.String) | |
| 740 | + { | |
| 741 | + var s = v.GetString(); | |
| 742 | + return string.IsNullOrWhiteSpace(s) ? "无" : s.Trim(); | |
| 743 | + } | |
| 744 | + | |
| 745 | + if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n)) | |
| 746 | + { | |
| 747 | + return n.ToString(CultureInfo.InvariantCulture); | |
| 748 | + } | |
| 749 | + | |
| 750 | + return v.ToString(); | |
| 751 | + } | |
| 752 | + } | |
| 753 | + catch | |
| 754 | + { | |
| 755 | + return "无"; | |
| 756 | + } | |
| 757 | + | |
| 758 | + return "无"; | |
| 759 | + } | |
| 760 | + | |
| 761 | + private static IActionResult BuildEmptyPdf(string fileName) | |
| 762 | + { | |
| 763 | + QuestPDF.Settings.License = LicenseType.Community; | |
| 764 | + var document = Document.Create(c => | |
| 765 | + { | |
| 766 | + c.Page(p => | |
| 767 | + { | |
| 768 | + p.Margin(30); | |
| 769 | + p.Content().Text("No data for current filters."); | |
| 770 | + }); | |
| 771 | + }); | |
| 772 | + var ms = new MemoryStream(); | |
| 773 | + document.GeneratePdf(ms); | |
| 774 | + ms.Position = 0; | |
| 775 | + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; | |
| 776 | + } | |
| 777 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
| 1 | 1 | using System; |
| 2 | 2 | using System.Collections.Generic; |
| 3 | +using System.Globalization; | |
| 3 | 4 | using System.IdentityModel.Tokens.Jwt; |
| 4 | 5 | using System.Linq; |
| 5 | 6 | using System.Security.Claims; |
| ... | ... | @@ -20,6 +21,7 @@ using Volo.Abp; |
| 20 | 21 | using Volo.Abp.Application.Services; |
| 21 | 22 | using Volo.Abp.EventBus.Local; |
| 22 | 23 | using Volo.Abp.Security.Claims; |
| 24 | +using Volo.Abp.Uow; | |
| 23 | 25 | using Volo.Abp.Users; |
| 24 | 26 | using Yi.Framework.Core.Helper; |
| 25 | 27 | using Yi.Framework.Rbac.Domain.Entities; |
| ... | ... | @@ -135,6 +137,267 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 135 | 137 | return await LoadBoundLocationsAsync(CurrentUser.Id.Value); |
| 136 | 138 | } |
| 137 | 139 | |
| 140 | + /// <summary> | |
| 141 | + /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户) | |
| 142 | + /// </summary> | |
| 143 | + /// <remarks> | |
| 144 | + /// 仅当当前登录用户在 <c>userlocation</c> 中绑定该 <c>locationId</c> 时可查;否则返回业务异常。 | |
| 145 | + /// | |
| 146 | + /// 店长:在同店绑定用户中,取 <c>Role.RoleCode</c> 或 <c>Role.RoleName</c>(忽略大小写)包含 <c>manager</c> 的第一条; | |
| 147 | + /// 若无匹配则店长姓名与电话均为「无」。 | |
| 148 | + /// | |
| 149 | + /// <c>OperatingHours</c>:当前 <c>location</c> 表无营业时间字段,固定返回「无」。 | |
| 150 | + /// </remarks> | |
| 151 | + /// <param name="locationId">门店主键(Guid 字符串)</param> | |
| 152 | + /// <returns>与原型一致的展示字段</returns> | |
| 153 | + /// <response code="200">成功</response> | |
| 154 | + /// <response code="400">未登录、门店标识无效、未绑定或门店不存在</response> | |
| 155 | + /// <response code="500">服务器错误</response> | |
| 156 | + [Authorize] | |
| 157 | + public virtual async Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId) | |
| 158 | + { | |
| 159 | + if (!CurrentUser.Id.HasValue) | |
| 160 | + { | |
| 161 | + throw new UserFriendlyException("用户未登录"); | |
| 162 | + } | |
| 163 | + | |
| 164 | + var lid = (locationId ?? string.Empty).Trim(); | |
| 165 | + if (string.IsNullOrEmpty(lid) || !Guid.TryParse(lid, out var locationGuid)) | |
| 166 | + { | |
| 167 | + throw new UserFriendlyException("无效的门店标识"); | |
| 168 | + } | |
| 169 | + | |
| 170 | + var userIdStr = CurrentUser.Id.Value.ToString(); | |
| 171 | + var bound = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 172 | + .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid); | |
| 173 | + if (!bound) | |
| 174 | + { | |
| 175 | + throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); | |
| 176 | + } | |
| 177 | + | |
| 178 | + var locRows = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | |
| 179 | + .Where(x => !x.IsDeleted && x.Id == locationGuid) | |
| 180 | + .ToListAsync(); | |
| 181 | + var loc = locRows.FirstOrDefault(); | |
| 182 | + if (loc is null) | |
| 183 | + { | |
| 184 | + throw new UserFriendlyException("门店不存在或已删除"); | |
| 185 | + } | |
| 186 | + | |
| 187 | + var (mgrName, mgrPhone) = await TryResolveStoreManagerAsync(lid); | |
| 188 | + | |
| 189 | + return new UsAppLocationDetailOutputDto | |
| 190 | + { | |
| 191 | + LocationId = loc.Id.ToString(), | |
| 192 | + LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(), | |
| 193 | + FullAddress = BuildFullAddress(loc), | |
| 194 | + StorePhone = FormatStorePhoneDisplay(loc.Phone), | |
| 195 | + OperatingHours = "无", | |
| 196 | + ManagerName = mgrName, | |
| 197 | + ManagerPhone = mgrPhone | |
| 198 | + }; | |
| 199 | + } | |
| 200 | + | |
| 201 | + /// <inheritdoc /> | |
| 202 | + [Authorize] | |
| 203 | + public virtual async Task<UsAppMyProfileOutputDto> GetMyProfileAsync() | |
| 204 | + { | |
| 205 | + if (!CurrentUser.Id.HasValue) | |
| 206 | + { | |
| 207 | + throw new UserFriendlyException("用户未登录"); | |
| 208 | + } | |
| 209 | + | |
| 210 | + var userId = CurrentUser.Id.Value; | |
| 211 | + var user = await _userRepository.GetByIdAsync(userId); | |
| 212 | + if (user is null || user.IsDeleted || !user.State) | |
| 213 | + { | |
| 214 | + throw new UserFriendlyException("用户不存在或已停用"); | |
| 215 | + } | |
| 216 | + | |
| 217 | + var roleRows = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity, RoleAggregateRoot>((ur, r) => ur.RoleId == r.Id) | |
| 218 | + .Where((ur, r) => ur.UserId == userId && !r.IsDeleted && r.State) | |
| 219 | + .OrderBy((ur, r) => r.OrderNum) | |
| 220 | + .Select((ur, r) => new { r.RoleName, r.RoleCode }) | |
| 221 | + .ToListAsync(); | |
| 222 | + | |
| 223 | + var roleNames = roleRows.Select(x => x.RoleName?.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); | |
| 224 | + var roleDisplay = roleNames.Count == 0 ? "无" : string.Join(", ", roleNames); | |
| 225 | + var primaryCode = roleRows.FirstOrDefault()?.RoleCode?.Trim(); | |
| 226 | + | |
| 227 | + var fullName = !string.IsNullOrWhiteSpace(user.Name?.Trim()) | |
| 228 | + ? user.Name.Trim() | |
| 229 | + : (!string.IsNullOrWhiteSpace(user.Nick?.Trim()) ? user.Nick.Trim() : user.UserName.Trim()); | |
| 230 | + | |
| 231 | + return new UsAppMyProfileOutputDto | |
| 232 | + { | |
| 233 | + FullName = fullName, | |
| 234 | + Email = string.IsNullOrWhiteSpace(user.Email) ? "无" : user.Email.Trim(), | |
| 235 | + Phone = FormatPhoneDisplay(user.Phone), | |
| 236 | + EmployeeId = string.IsNullOrWhiteSpace(user.UserName) ? "无" : user.UserName.Trim(), | |
| 237 | + RoleDisplay = roleDisplay, | |
| 238 | + PrimaryRoleCode = string.IsNullOrWhiteSpace(primaryCode) ? null : primaryCode | |
| 239 | + }; | |
| 240 | + } | |
| 241 | + | |
| 242 | + /// <inheritdoc /> | |
| 243 | + [Authorize] | |
| 244 | + [UnitOfWork] | |
| 245 | + public virtual async Task ChangePasswordAsync(UsAppChangePasswordInputVo input) | |
| 246 | + { | |
| 247 | + if (input is null) | |
| 248 | + { | |
| 249 | + throw new UserFriendlyException("入参不能为空"); | |
| 250 | + } | |
| 251 | + | |
| 252 | + if (!CurrentUser.Id.HasValue) | |
| 253 | + { | |
| 254 | + throw new UserFriendlyException("用户未登录"); | |
| 255 | + } | |
| 256 | + | |
| 257 | + var current = input.CurrentPassword ?? string.Empty; | |
| 258 | + var newPwd = input.NewPassword ?? string.Empty; | |
| 259 | + var confirm = input.ConfirmNewPassword ?? string.Empty; | |
| 260 | + | |
| 261 | + if (string.IsNullOrWhiteSpace(current) || string.IsNullOrWhiteSpace(newPwd) || string.IsNullOrWhiteSpace(confirm)) | |
| 262 | + { | |
| 263 | + throw new UserFriendlyException("请填写当前密码、新密码与确认密码"); | |
| 264 | + } | |
| 265 | + | |
| 266 | + if (!string.Equals(newPwd, confirm, StringComparison.Ordinal)) | |
| 267 | + { | |
| 268 | + throw new UserFriendlyException("新密码与确认密码不一致"); | |
| 269 | + } | |
| 270 | + | |
| 271 | + if (string.Equals(current, newPwd, StringComparison.Ordinal)) | |
| 272 | + { | |
| 273 | + throw new UserFriendlyException("新密码不能与当前密码相同"); | |
| 274 | + } | |
| 275 | + | |
| 276 | + ValidateAppPasswordComplexity(newPwd); | |
| 277 | + | |
| 278 | + var userId = CurrentUser.Id.Value; | |
| 279 | + var user = await _userRepository.GetByIdAsync(userId); | |
| 280 | + if (user is null || user.IsDeleted || !user.State) | |
| 281 | + { | |
| 282 | + throw new UserFriendlyException("用户不存在或已停用"); | |
| 283 | + } | |
| 284 | + | |
| 285 | + if (!user.JudgePassword(current)) | |
| 286 | + { | |
| 287 | + throw new UserFriendlyException(UserConst.Login_Error); | |
| 288 | + } | |
| 289 | + | |
| 290 | + user.EncryPassword.Password = newPwd; | |
| 291 | + user.BuildPassword(); | |
| 292 | + await _userRepository.UpdateAsync(user); | |
| 293 | + } | |
| 294 | + | |
| 295 | + private static string FormatPhoneDisplay(long? phone) | |
| 296 | + { | |
| 297 | + if (!phone.HasValue) | |
| 298 | + { | |
| 299 | + return "无"; | |
| 300 | + } | |
| 301 | + | |
| 302 | + var digits = Math.Abs(phone.Value).ToString(CultureInfo.InvariantCulture); | |
| 303 | + if (digits.Length == 10) | |
| 304 | + { | |
| 305 | + return $"+1 ({digits[..3]}) {digits.Substring(3, 3)}-{digits.Substring(6, 4)}"; | |
| 306 | + } | |
| 307 | + | |
| 308 | + if (digits.Length == 11 && digits.StartsWith("1", StringComparison.Ordinal)) | |
| 309 | + { | |
| 310 | + return $"+1 ({digits[1..4]}) {digits.Substring(4, 3)}-{digits.Substring(7, 4)}"; | |
| 311 | + } | |
| 312 | + | |
| 313 | + return $"+{digits}"; | |
| 314 | + } | |
| 315 | + | |
| 316 | + private static string FormatStorePhoneDisplay(string? phone) | |
| 317 | + { | |
| 318 | + var t = phone?.Trim(); | |
| 319 | + return string.IsNullOrEmpty(t) ? "无" : t; | |
| 320 | + } | |
| 321 | + | |
| 322 | + private async Task<(string Name, string Phone)> TryResolveStoreManagerAsync(string locationIdTrimmed) | |
| 323 | + { | |
| 324 | + var userIdStrings = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 325 | + .Where(x => !x.IsDeleted && x.LocationId == locationIdTrimmed) | |
| 326 | + .Select(x => x.UserId) | |
| 327 | + .Distinct() | |
| 328 | + .ToListAsync(); | |
| 329 | + | |
| 330 | + var userGuids = userIdStrings | |
| 331 | + .Select(s => Guid.TryParse(s, out var g) ? (Guid?)g : null) | |
| 332 | + .Where(g => g.HasValue) | |
| 333 | + .Select(g => g!.Value) | |
| 334 | + .ToList(); | |
| 335 | + | |
| 336 | + if (userGuids.Count == 0) | |
| 337 | + { | |
| 338 | + return ("无", "无"); | |
| 339 | + } | |
| 340 | + | |
| 341 | + var rows = await _dbContext.SqlSugarClient.Queryable<UserAggregateRoot, UserRoleEntity, RoleAggregateRoot>( | |
| 342 | + (u, ur, r) => u.Id == ur.UserId && ur.RoleId == r.Id) | |
| 343 | + .Where((u, ur, r) => userGuids.Contains(u.Id) && !u.IsDeleted && u.State) | |
| 344 | + .Where((u, ur, r) => !r.IsDeleted && r.State) | |
| 345 | + .Where((u, ur, r) => | |
| 346 | + SqlFunc.ToLower(r.RoleCode).Contains("manager") || | |
| 347 | + SqlFunc.ToLower(r.RoleName).Contains("manager")) | |
| 348 | + .OrderBy((u, ur, r) => u.Name) | |
| 349 | + .Select((u, ur, r) => new | |
| 350 | + { | |
| 351 | + u.Name, | |
| 352 | + u.Nick, | |
| 353 | + u.UserName, | |
| 354 | + u.Phone | |
| 355 | + }) | |
| 356 | + .ToListAsync(); | |
| 357 | + | |
| 358 | + var row = rows.FirstOrDefault(); | |
| 359 | + if (row is null) | |
| 360 | + { | |
| 361 | + return ("无", "无"); | |
| 362 | + } | |
| 363 | + | |
| 364 | + var displayName = !string.IsNullOrWhiteSpace(row.Name?.Trim()) | |
| 365 | + ? row.Name!.Trim() | |
| 366 | + : (!string.IsNullOrWhiteSpace(row.Nick?.Trim()) | |
| 367 | + ? row.Nick!.Trim() | |
| 368 | + : (row.UserName?.Trim() ?? "无")); | |
| 369 | + | |
| 370 | + return (displayName, FormatPhoneDisplay(row.Phone)); | |
| 371 | + } | |
| 372 | + | |
| 373 | + private static void ValidateAppPasswordComplexity(string password) | |
| 374 | + { | |
| 375 | + if (password.Length < 8) | |
| 376 | + { | |
| 377 | + throw new UserFriendlyException("新密码至少 8 位"); | |
| 378 | + } | |
| 379 | + | |
| 380 | + if (!password.Any(char.IsUpper)) | |
| 381 | + { | |
| 382 | + throw new UserFriendlyException("新密码需包含大写字母"); | |
| 383 | + } | |
| 384 | + | |
| 385 | + if (!password.Any(char.IsLower)) | |
| 386 | + { | |
| 387 | + throw new UserFriendlyException("新密码需包含小写字母"); | |
| 388 | + } | |
| 389 | + | |
| 390 | + if (!password.Any(char.IsDigit)) | |
| 391 | + { | |
| 392 | + throw new UserFriendlyException("新密码需包含至少一个数字"); | |
| 393 | + } | |
| 394 | + | |
| 395 | + if (!password.Any(c => !char.IsLetterOrDigit(c))) | |
| 396 | + { | |
| 397 | + throw new UserFriendlyException("新密码需包含至少一个特殊字符"); | |
| 398 | + } | |
| 399 | + } | |
| 400 | + | |
| 138 | 401 | private void ValidationImageCaptcha(string? uuid, string? code) |
| 139 | 402 | { |
| 140 | 403 | if (!_rbacOptions.EnableCaptcha) | ... | ... |
美国版/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 |
| 613 | 613 | throw new UserFriendlyException("打印任务不存在"); |
| 614 | 614 | } |
| 615 | 615 | |
| 616 | - // 仅允许重打自己在当前门店的任务 | |
| 617 | - if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) | |
| 616 | + // 非 admin:仅允许重打自己在当前门店的任务;admin 可重打任意用户任务(仍须门店一致) | |
| 617 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 618 | + if (!isAdmin && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) | |
| 618 | 619 | { |
| 619 | 620 | throw new UserFriendlyException("无权限重打该任务"); |
| 620 | 621 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql
0 → 100644
| 1 | +-- 组织/分组(Group),归属合作伙伴(Parent Partner) | |
| 2 | +-- 依赖:请先执行 fl_partner_create.sql,保证存在 fl_partner 表。 | |
| 3 | + | |
| 4 | +CREATE TABLE IF NOT EXISTS `fl_group` ( | |
| 5 | + `Id` varchar(64) NOT NULL COMMENT '主键', | |
| 6 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 7 | + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', | |
| 8 | + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', | |
| 9 | + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', | |
| 10 | + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', | |
| 11 | + `GroupName` varchar(256) NOT NULL COMMENT '组织名称', | |
| 12 | + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id', | |
| 13 | + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | |
| 14 | + PRIMARY KEY (`Id`), | |
| 15 | + KEY `IX_fl_group_IsDeleted` (`IsDeleted`), | |
| 16 | + KEY `IX_fl_group_State` (`State`), | |
| 17 | + KEY `IX_fl_group_PartnerId` (`PartnerId`), | |
| 18 | + KEY `IX_fl_group_GroupName` (`GroupName`(128)), | |
| 19 | + KEY `IX_fl_group_CreationTime` (`CreationTime`), | |
| 20 | + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`) | |
| 21 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)'; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql
0 → 100644
| 1 | +-- 合作伙伴主数据(美国版 antis-foodlabeling-us) | |
| 2 | +-- 执行前请确认连接库为业务库;若表已存在请勿重复执行。 | |
| 3 | + | |
| 4 | +CREATE TABLE IF NOT EXISTS `fl_partner` ( | |
| 5 | + `Id` varchar(64) NOT NULL COMMENT '主键', | |
| 6 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 7 | + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', | |
| 8 | + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', | |
| 9 | + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', | |
| 10 | + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', | |
| 11 | + `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称', | |
| 12 | + `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱', | |
| 13 | + `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话', | |
| 14 | + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | |
| 15 | + PRIMARY KEY (`Id`), | |
| 16 | + KEY `IX_fl_partner_IsDeleted` (`IsDeleted`), | |
| 17 | + KEY `IX_fl_partner_State` (`State`), | |
| 18 | + KEY `IX_fl_partner_CreationTime` (`CreationTime`), | |
| 19 | + KEY `IX_fl_partner_PartnerName` (`PartnerName`(128)) | |
| 20 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合作伙伴'; | ... | ... |
项目相关文档/Dashboard统计接口对接说明.md
| ... | ... | @@ -28,6 +28,7 @@ |
| 28 | 28 | "weeklyPrintVolume": [], |
| 29 | 29 | "byCategory": [], |
| 30 | 30 | "byCategoryTotal": 0, |
| 31 | + "recentLabels": [], | |
| 31 | 32 | "generatedAt": "2026-04-22T10:00:00+08:00", |
| 32 | 33 | |
| 33 | 34 | "metricCards": [], |
| ... | ... | @@ -39,6 +40,7 @@ |
| 39 | 40 | 说明: |
| 40 | 41 | - `labelsPrintedToday/activeTemplates/...`、`byCategory/byCategoryTotal` 是**前端直观命名**(推荐使用)。 |
| 41 | 42 | - `metricCards`、`categoryDistribution`、`categoryDistributionTotal` 为**兼容字段**(与旧版返回一致)。 |
| 43 | +- `recentLabels`:**Recent Labels** 区块数据,全门店按打印时间倒序取最新 **10** 条(`fl_label_print_task`)。 | |
| 42 | 44 | |
| 43 | 45 | --- |
| 44 | 46 | |
| ... | ... | @@ -88,6 +90,25 @@ |
| 88 | 90 | |
| 89 | 91 | --- |
| 90 | 92 | |
| 93 | +### 3.4 最近打印标签(`recentLabels`) | |
| 94 | + | |
| 95 | +数组元素类型:`DashboardRecentLabelItemDto`,按 **`PrintedAt`(`PrintedAt` 为空则用 `CreationTime`)倒序**,最多 **10** 条。 | |
| 96 | + | |
| 97 | +| 字段 | 类型 | 说明 | | |
| 98 | +|---|---|---| | |
| 99 | +| `taskId` | string | 打印任务 Id(`fl_label_print_task.Id`) | | |
| 100 | +| `labelCode` | string | 标签编码(界面 Serial,如 `1-251201`) | | |
| 101 | +| `displayName` | string | 展示标题:优先 **产品名**,否则 **标签名** | | |
| 102 | +| `printedByUserId` | string \| null | 打印人用户 Id(`CreatedBy`) | | |
| 103 | +| `printedByName` | string | 打印人展示名(`User.Name` 或 `UserName`) | | |
| 104 | +| `printedAt` | string (datetime) | 打印时间(ISO 8601) | | |
| 105 | +| `status` | string | `active` 或 `expired`:从 `PrintInputJson` 解析 `expiryDate` / `expiry` / `expirationDate`,与**当天日期**比较;无保质期或解析失败视为 `active` | | |
| 106 | +| `labelTypeBadge` | string | 模板尺寸短文案(如 `2"x2"`),用于左侧圆标 | | |
| 107 | + | |
| 108 | +前端可用 `printedAt` 自行格式化为「10 mins ago」等相对时间。 | |
| 109 | + | |
| 110 | +--- | |
| 111 | + | |
| 91 | 112 | ## 4. 返回示例 |
| 92 | 113 | |
| 93 | 114 | ```json |
| ... | ... | @@ -155,6 +176,18 @@ |
| 155 | 176 | { "categoryId": "CAT003", "categoryName": "Dinner", "count": 230, "ratio": 23.00 } |
| 156 | 177 | ], |
| 157 | 178 | "byCategoryTotal": 1000, |
| 179 | + "recentLabels": [ | |
| 180 | + { | |
| 181 | + "taskId": "…", | |
| 182 | + "labelCode": "1-251201", | |
| 183 | + "displayName": "Chicken Breast", | |
| 184 | + "printedByUserId": "…", | |
| 185 | + "printedByName": "Alice J.", | |
| 186 | + "printedAt": "2026-04-22T09:50:00+08:00", | |
| 187 | + "status": "active", | |
| 188 | + "labelTypeBadge": "2\"x2\"" | |
| 189 | + } | |
| 190 | + ], | |
| 158 | 191 | "generatedAt": "2026-04-22T10:00:00+08:00", |
| 159 | 192 | "metricCards": [], |
| 160 | 193 | "categoryDistribution": [], | ... | ... |
项目相关文档/合作伙伴Partner接口对接说明.md
0 → 100644
| 1 | +# 合作伙伴(Partner)与组织(Group)接口对接说明 | |
| 2 | + | |
| 3 | +> 适用范围:美国版 Web 管理端「Account Management」下的 **Partner**、**Group** 主数据 | |
| 4 | +> **Partner** 表:`fl_partner`,接口:`IPartnerAppService` / `PartnerAppService` | |
| 5 | +> **Group** 表:`fl_group`(`PartnerId` 关联 `fl_partner.Id`),接口:`IGroupAppService` / `GroupAppService` | |
| 6 | +> 宿主路由前缀:`/api/app`(与 `YiAbpWebModule` 中 `RootPath = api/app` 一致) | |
| 7 | + | |
| 8 | +--- | |
| 9 | + | |
| 10 | +## 0. 通用说明 | |
| 11 | + | |
| 12 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`),与其它 `/api/app/*` 接口一致。 | |
| 13 | +- **Content-Type**:`POST` / `PUT` 使用 `application/json`。 | |
| 14 | +- **分页约定(美国版食品标签模块)**:`skipCount` 表示**页码(从 1 起)**,不是 0 基 offset;第一页请传 `skipCount=1`。与 `PagedQueryConvention` 及 SqlSugar `ToPageListAsync` 用法一致。 | |
| 15 | +- **逻辑删除**:`DELETE` 将对应表的 `IsDeleted` 置为 `true`(`fl_partner` / `fl_group`),不物理删行。 | |
| 16 | +- **列表与导出筛选一致**:各模块列表的筛选字段与对应 **export-pdf** 接口一致,便于数据对齐。 | |
| 17 | +- **Group 与 Partner**:新建/编辑 Group 时 `partnerId` 必须指向**未逻辑删除**的 `fl_partner`;列表左联 Partner 时仅展示未删除合作伙伴名称,已删 Partner 在列表中显示为 **`无`**。 | |
| 18 | + | |
| 19 | +--- | |
| 20 | + | |
| 21 | +# 第一部分:Partner(合作伙伴) | |
| 22 | + | |
| 23 | +## 1. 分页列表 | |
| 24 | + | |
| 25 | +- **方法**:`GET` | |
| 26 | +- **路径**:`/api/app/partner` | |
| 27 | + | |
| 28 | +### 1.1 查询参数(`PartnerGetListInputVo`) | |
| 29 | + | |
| 30 | +| 参数 | 类型 | 必填 | 说明 | | |
| 31 | +|------|------|------|------| | |
| 32 | +| `skipCount` | int | 是 | 页码,从 **1** 开始 | | |
| 33 | +| `maxResultCount` | int | 是 | 每页条数 | | |
| 34 | +| `sorting` | string | 否 | 排序,仅支持白名单(见下) | | |
| 35 | +| `keyword` | string | 否 | 模糊匹配 `PartnerName`、`ContactEmail`、`PhoneNumber` | | |
| 36 | +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 | | |
| 37 | + | |
| 38 | +**排序白名单**(大小写不敏感): | |
| 39 | + | |
| 40 | +- `PartnerName asc` / `PartnerName desc` | |
| 41 | +- `CreationTime asc` / `CreationTime desc` | |
| 42 | +- `State asc` / `State desc` | |
| 43 | + | |
| 44 | +其它值将回退为默认:**按 `CreationTime` 降序**。 | |
| 45 | + | |
| 46 | +### 1.2 请求示例 | |
| 47 | + | |
| 48 | +```http | |
| 49 | +GET /api/app/partner?skipCount=1&maxResultCount=10&keyword=Global&state=true&sorting=CreationTime%20desc HTTP/1.1 | |
| 50 | +Authorization: Bearer {token} | |
| 51 | +``` | |
| 52 | + | |
| 53 | +### 1.3 响应结构(`PagedResultWithPageDto<PartnerGetListOutputDto>`) | |
| 54 | + | |
| 55 | +```json | |
| 56 | +{ | |
| 57 | + "pageIndex": 1, | |
| 58 | + "pageSize": 10, | |
| 59 | + "totalCount": 2, | |
| 60 | + "totalPages": 1, | |
| 61 | + "items": [ | |
| 62 | + { | |
| 63 | + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", | |
| 64 | + "partnerName": "Global Foods Inc.", | |
| 65 | + "contactEmail": "admin@globalfoods.com", | |
| 66 | + "phoneNumber": "+1 (555) 100-2000", | |
| 67 | + "state": true, | |
| 68 | + "creationTime": "2026-04-27T10:00:00" | |
| 69 | + } | |
| 70 | + ] | |
| 71 | +} | |
| 72 | +``` | |
| 73 | + | |
| 74 | +### 1.4 列表项字段(`PartnerGetListOutputDto`) | |
| 75 | + | |
| 76 | +| 字段 | 类型 | 说明 | | |
| 77 | +|------|------|------| | |
| 78 | +| `id` | string | 主键 | | |
| 79 | +| `partnerName` | string | 合作伙伴名称 | | |
| 80 | +| `contactEmail` | string \| null | 联系邮箱 | | |
| 81 | +| `phoneNumber` | string \| null | 电话 | | |
| 82 | +| `state` | bool | 是否启用(UI Active 开关) | | |
| 83 | +| `creationTime` | string (datetime) | 创建时间 | | |
| 84 | + | |
| 85 | +--- | |
| 86 | + | |
| 87 | +## 2. 详情 | |
| 88 | + | |
| 89 | +- **方法**:`GET` | |
| 90 | +- **路径**:`/api/app/partner/{id}` | |
| 91 | + | |
| 92 | +### 2.1 路径参数 | |
| 93 | + | |
| 94 | +| 参数 | 说明 | | |
| 95 | +|------|------| | |
| 96 | +| `id` | `fl_partner.Id` | | |
| 97 | + | |
| 98 | +### 2.2 响应结构(`PartnerGetOutputDto`) | |
| 99 | + | |
| 100 | +| 字段 | 类型 | 说明 | | |
| 101 | +|------|------|------| | |
| 102 | +| `id` | string | 主键 | | |
| 103 | +| `partnerName` | string | 合作伙伴名称 | | |
| 104 | +| `contactEmail` | string \| null | 联系邮箱 | | |
| 105 | +| `phoneNumber` | string \| null | 电话 | | |
| 106 | +| `state` | bool | 是否启用 | | |
| 107 | +| `creationTime` | string (datetime) | 创建时间 | | |
| 108 | +| `lastModificationTime` | string (datetime) \| null | 最后修改时间 | | |
| 109 | + | |
| 110 | +### 2.3 错误说明 | |
| 111 | + | |
| 112 | +- `id` 为空或记录不存在(含已逻辑删除):业务错误提示「合作伙伴不存在」等。 | |
| 113 | + | |
| 114 | +--- | |
| 115 | + | |
| 116 | +## 3. 新增 | |
| 117 | + | |
| 118 | +- **方法**:`POST` | |
| 119 | +- **路径**:`/api/app/partner` | |
| 120 | + | |
| 121 | +### 3.1 Body(`PartnerCreateInputVo`) | |
| 122 | + | |
| 123 | +```json | |
| 124 | +{ | |
| 125 | + "partnerName": "Global Foods Inc.", | |
| 126 | + "contactEmail": "admin@globalfoods.com", | |
| 127 | + "phoneNumber": "+1 (555) 100-2000", | |
| 128 | + "state": true | |
| 129 | +} | |
| 130 | +``` | |
| 131 | + | |
| 132 | +| 字段 | 类型 | 必填 | 说明 | | |
| 133 | +|------|------|------|------| | |
| 134 | +| `partnerName` | string | 是 | 合作伙伴名称,去首尾空格后不能为空 | | |
| 135 | +| `contactEmail` | string | 否 | 若填写则做简单格式校验(含 `@` 等) | | |
| 136 | +| `phoneNumber` | string | 否 | 电话 | | |
| 137 | +| `state` | bool | 否 | 默认 `true` | | |
| 138 | + | |
| 139 | +### 3.2 响应 | |
| 140 | + | |
| 141 | +- 成功:返回 `PartnerGetOutputDto`(与详情结构一致)。 | |
| 142 | + | |
| 143 | +--- | |
| 144 | + | |
| 145 | +## 4. 编辑 | |
| 146 | + | |
| 147 | +- **方法**:`PUT` | |
| 148 | +- **路径**:`/api/app/partner/{id}` | |
| 149 | + | |
| 150 | +### 4.1 参数 | |
| 151 | + | |
| 152 | +- **Path**:`id` 为当前合作伙伴主键。 | |
| 153 | +- **Body**:`PartnerUpdateInputVo`,字段与新增相同。 | |
| 154 | + | |
| 155 | +```json | |
| 156 | +{ | |
| 157 | + "partnerName": "Global Foods Inc.", | |
| 158 | + "contactEmail": "admin@globalfoods.com", | |
| 159 | + "phoneNumber": "+1 (555) 100-2000", | |
| 160 | + "state": false | |
| 161 | +} | |
| 162 | +``` | |
| 163 | + | |
| 164 | +### 4.2 响应 | |
| 165 | + | |
| 166 | +- 成功:返回更新后的 `PartnerGetOutputDto`。 | |
| 167 | + | |
| 168 | +--- | |
| 169 | + | |
| 170 | +## 5. 删除(逻辑删除) | |
| 171 | + | |
| 172 | +- **方法**:`DELETE` | |
| 173 | +- **路径**:`/api/app/partner/{id}` | |
| 174 | + | |
| 175 | +### 5.1 路径参数 | |
| 176 | + | |
| 177 | +| 参数 | 说明 | | |
| 178 | +|------|------| | |
| 179 | +| `id` | `fl_partner.Id` | | |
| 180 | + | |
| 181 | +### 5.2 行为 | |
| 182 | + | |
| 183 | +- 将 `IsDeleted` 置为 `true`,并更新 `LastModificationTime` / `LastModifierId`(若当前用户存在)。 | |
| 184 | + | |
| 185 | +--- | |
| 186 | + | |
| 187 | +## 6. 批量导出 PDF | |
| 188 | + | |
| 189 | +- **方法**:`GET` | |
| 190 | +- **路径**:`/api/app/partner/export-pdf` | |
| 191 | +- **响应**:`Content-Type: application/pdf`,附件名形如 `partners_yyyy-MM-dd_HH-mm-ss.pdf` | |
| 192 | + | |
| 193 | +### 6.1 查询参数 | |
| 194 | + | |
| 195 | +与列表接口相同的筛选字段(分页字段可忽略): | |
| 196 | + | |
| 197 | +| 参数 | 类型 | 必填 | 说明 | | |
| 198 | +|------|------|------|------| | |
| 199 | +| `keyword` | string | 否 | 与列表 `keyword` 一致 | | |
| 200 | +| `state` | bool | 否 | 与列表 `state` 一致 | | |
| 201 | +| `sorting` | string | 否 | 与列表白名单一致,用于导出行顺序 | | |
| 202 | + | |
| 203 | +### 6.2 限制 | |
| 204 | + | |
| 205 | +- 命中行数 **超过 5000** 时接口返回业务错误,需缩小筛选范围后再导出。 | |
| 206 | +- 导出最多取 **5000** 条,排序与列表查询逻辑一致。 | |
| 207 | + | |
| 208 | +### 6.3 请求示例 | |
| 209 | + | |
| 210 | +```http | |
| 211 | +GET /api/app/partner/export-pdf?keyword=Global&state=true HTTP/1.1 | |
| 212 | +Authorization: Bearer {token} | |
| 213 | +``` | |
| 214 | + | |
| 215 | +### 6.4 PDF 内容说明 | |
| 216 | + | |
| 217 | +- 表头列:**Partner**、**Contact**、**Phone**、**Status**、**Created**。 | |
| 218 | +- `Status` 文本:`state === true` 时为 `active`,否则 `inactive`。 | |
| 219 | +- 空邮箱、空电话在 PDF 中显示为 **`无`**(与项目列表空值展示约定一致)。 | |
| 220 | + | |
| 221 | +--- | |
| 222 | + | |
| 223 | +## 7. 数据库与建表(Partner) | |
| 224 | + | |
| 225 | +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql` | |
| 226 | +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`PartnerName`、`ContactEmail`、`PhoneNumber`、`State` | |
| 227 | + | |
| 228 | +--- | |
| 229 | + | |
| 230 | +## 8. 与门店字段的关系说明 | |
| 231 | + | |
| 232 | +- 门店(`location`)上可能存在 **`Partner` 字符串字段**(原型/筛选用),与本文 **`fl_partner` 主数据表** 无强制外键关联。 | |
| 233 | +- 若后续要将门店关联到合作伙伴主数据,需单独产品方案(例如增加 `PartnerId` 或同步名称)。 | |
| 234 | + | |
| 235 | +--- | |
| 236 | + | |
| 237 | +## 9. 前端对接提示(Partner) | |
| 238 | + | |
| 239 | +- 列表「Search」对应 `keyword`;「Active」筛选对应 `state`。 | |
| 240 | +- 「Bulk Export (PDF)」调用 **第 6 节** 导出接口,查询参数与当前列表筛选保持一致即可。 | |
| 241 | + | |
| 242 | +--- | |
| 243 | + | |
| 244 | +# 第二部分:Group(组织 / Group) | |
| 245 | + | |
| 246 | +> UI:**Group Name**、**Parent Partner**(下拉绑定合作伙伴)、**Status**、**Bulk Export (PDF)**、**New+** 弹窗(Group Name、Assign to Partner、Active)。 | |
| 247 | + | |
| 248 | +## 10. 数据库与建表(Group) | |
| 249 | + | |
| 250 | +- 库中原先**无**独立 Group 业务表;新建表名:`fl_group`。 | |
| 251 | +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql` | |
| 252 | +- **须先存在 `fl_partner` 表**(脚本内含外键 `FK_fl_group_partner` → `fl_partner(Id)`)。 | |
| 253 | +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`GroupName`、`PartnerId`、`State` | |
| 254 | + | |
| 255 | +**建表 SQL(与脚本文件一致,便于直接执行):** | |
| 256 | + | |
| 257 | +```sql | |
| 258 | +CREATE TABLE IF NOT EXISTS `fl_group` ( | |
| 259 | + `Id` varchar(64) NOT NULL COMMENT '主键', | |
| 260 | + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', | |
| 261 | + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间', | |
| 262 | + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人', | |
| 263 | + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间', | |
| 264 | + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人', | |
| 265 | + `GroupName` varchar(256) NOT NULL COMMENT '组织名称', | |
| 266 | + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id', | |
| 267 | + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | |
| 268 | + PRIMARY KEY (`Id`), | |
| 269 | + KEY `IX_fl_group_IsDeleted` (`IsDeleted`), | |
| 270 | + KEY `IX_fl_group_State` (`State`), | |
| 271 | + KEY `IX_fl_group_PartnerId` (`PartnerId`), | |
| 272 | + KEY `IX_fl_group_GroupName` (`GroupName`(128)), | |
| 273 | + KEY `IX_fl_group_CreationTime` (`CreationTime`), | |
| 274 | + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`) | |
| 275 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)'; | |
| 276 | +``` | |
| 277 | + | |
| 278 | +--- | |
| 279 | + | |
| 280 | +## 11. Group — 分页列表 | |
| 281 | + | |
| 282 | +- **方法**:`GET` | |
| 283 | +- **路径**:`/api/app/group` | |
| 284 | + | |
| 285 | +### 11.1 查询参数(`GroupGetListInputVo`) | |
| 286 | + | |
| 287 | +| 参数 | 类型 | 必填 | 说明 | | |
| 288 | +|------|------|------|------| | |
| 289 | +| `skipCount` | int | 是 | 页码,从 **1** 开始 | | |
| 290 | +| `maxResultCount` | int | 是 | 每页条数 | | |
| 291 | +| `sorting` | string | 否 | 排序白名单(见下) | | |
| 292 | +| `keyword` | string | 否 | 模糊匹配 `GroupName`、所属 **未删除** Partner 的 `PartnerName` | | |
| 293 | +| `partnerId` | string | 否 | 仅查看某合作伙伴下的组织(`fl_partner.Id`) | | |
| 294 | +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 | | |
| 295 | + | |
| 296 | +**排序白名单**(大小写不敏感): | |
| 297 | + | |
| 298 | +- `GroupName asc` / `GroupName desc` | |
| 299 | +- `CreationTime asc` / `CreationTime desc` | |
| 300 | +- `State asc` / `State desc` | |
| 301 | +- `PartnerName asc` / `PartnerName desc`(按关联合作伙伴名称) | |
| 302 | + | |
| 303 | +其它值回退为默认:**按 `CreationTime` 降序**。 | |
| 304 | + | |
| 305 | +### 11.2 请求示例 | |
| 306 | + | |
| 307 | +```http | |
| 308 | +GET /api/app/group?skipCount=1&maxResultCount=10&keyword=West&partnerId={partnerGuid}&state=true HTTP/1.1 | |
| 309 | +Authorization: Bearer {token} | |
| 310 | +``` | |
| 311 | + | |
| 312 | +### 11.3 响应结构(`PagedResultWithPageDto<GroupGetListOutputDto>`) | |
| 313 | + | |
| 314 | +```json | |
| 315 | +{ | |
| 316 | + "pageIndex": 1, | |
| 317 | + "pageSize": 10, | |
| 318 | + "totalCount": 2, | |
| 319 | + "totalPages": 1, | |
| 320 | + "items": [ | |
| 321 | + { | |
| 322 | + "id": "…", | |
| 323 | + "groupName": "West Coast Region", | |
| 324 | + "partnerId": "…", | |
| 325 | + "partnerName": "Global Foods Inc.", | |
| 326 | + "state": true, | |
| 327 | + "creationTime": "2026-04-27T10:00:00" | |
| 328 | + } | |
| 329 | + ] | |
| 330 | +} | |
| 331 | +``` | |
| 332 | + | |
| 333 | +### 11.4 列表项字段 | |
| 334 | + | |
| 335 | +| 字段 | 类型 | 说明 | | |
| 336 | +|------|------|------| | |
| 337 | +| `id` | string | 主键 | | |
| 338 | +| `groupName` | string | 组织名称 | | |
| 339 | +| `partnerId` | string | 所属合作伙伴 Id | | |
| 340 | +| `partnerName` | string | 父级合作伙伴名称(UI「Parent Partner」) | | |
| 341 | +| `state` | bool | 是否启用 | | |
| 342 | +| `creationTime` | string (datetime) | 创建时间 | | |
| 343 | + | |
| 344 | +--- | |
| 345 | + | |
| 346 | +## 12. Group — 详情 | |
| 347 | + | |
| 348 | +- **方法**:`GET` | |
| 349 | +- **路径**:`/api/app/group/{id}` | |
| 350 | + | |
| 351 | +路径参数 `id`:`fl_group.Id`。响应为 `GroupGetOutputDto`(在列表字段基础上增加 `lastModificationTime`)。 | |
| 352 | + | |
| 353 | +--- | |
| 354 | + | |
| 355 | +## 13. Group — 新增 | |
| 356 | + | |
| 357 | +- **方法**:`POST` | |
| 358 | +- **路径**:`/api/app/group` | |
| 359 | + | |
| 360 | +### Body(`GroupCreateInputVo`) | |
| 361 | + | |
| 362 | +```json | |
| 363 | +{ | |
| 364 | + "groupName": "West Coast Region", | |
| 365 | + "partnerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", | |
| 366 | + "state": true | |
| 367 | +} | |
| 368 | +``` | |
| 369 | + | |
| 370 | +| 字段 | 类型 | 必填 | 说明 | | |
| 371 | +|------|------|------|------| | |
| 372 | +| `groupName` | string | 是 | 组织名称 | | |
| 373 | +| `partnerId` | string | 是 | Assign to Partner,须为未删除的 `fl_partner.Id` | | |
| 374 | +| `state` | bool | 否 | 默认 `true` | | |
| 375 | + | |
| 376 | +--- | |
| 377 | + | |
| 378 | +## 14. Group — 编辑 | |
| 379 | + | |
| 380 | +- **方法**:`PUT` | |
| 381 | +- **路径**:`/api/app/group/{id}` | |
| 382 | +- **Body**:`GroupUpdateInputVo`(字段同新增) | |
| 383 | + | |
| 384 | +--- | |
| 385 | + | |
| 386 | +## 15. Group — 删除(逻辑删除) | |
| 387 | + | |
| 388 | +- **方法**:`DELETE` | |
| 389 | +- **路径**:`/api/app/group/{id}` | |
| 390 | + | |
| 391 | +--- | |
| 392 | + | |
| 393 | +## 16. Group — 批量导出 PDF | |
| 394 | + | |
| 395 | +- **方法**:`GET` | |
| 396 | +- **路径**:`/api/app/group/export-pdf` | |
| 397 | +- **响应**:`application/pdf`,附件名形如 `groups_yyyy-MM-dd_HH-mm-ss.pdf` | |
| 398 | + | |
| 399 | +### 16.1 查询参数 | |
| 400 | + | |
| 401 | +与列表一致(分页可忽略):`keyword`、`partnerId`、`state`、`sorting`。 | |
| 402 | + | |
| 403 | +### 16.2 限制 | |
| 404 | + | |
| 405 | +- 命中行数 **超过 5000** 返回业务错误;导出最多 **5000** 条。 | |
| 406 | + | |
| 407 | +### 16.3 PDF 列 | |
| 408 | + | |
| 409 | +**Group Name**、**Parent Partner**、**Status**(`active` / `inactive`)、**Created**。 | |
| 410 | + | |
| 411 | +--- | |
| 412 | + | |
| 413 | +## 17. 前端对接提示(Group) | |
| 414 | + | |
| 415 | +- 「Search」→ `keyword`;按父级合作伙伴筛选 → `partnerId`(下拉选中项的 `id`);「Active」→ `state`。 | |
| 416 | +- 「Assign to Partner」下拉数据来自 **Partner 列表接口**(`/api/app/partner`)。 | |
| 417 | +- 「Bulk Export (PDF)」→ **第 16 节**,查询参数与当前列表筛选一致。 | ... | ... |
项目相关文档/报表Reports接口对接说明.md
0 → 100644
| 1 | +# 报表 Reports 接口对接说明(Print Log / Label Report) | |
| 2 | + | |
| 3 | +> 适用范围:美国版 Web「Reports」— **Print Log** 列表与导出、**Label Report** 统计与导出、**重打** | |
| 4 | +> 实现:`IReportsAppService` / `ReportsAppService`;重打复用 `IUsAppLabelingAppService.ReprintAsync`(已支持 admin 跳过创建人校验) | |
| 5 | +> 路由前缀:`/api/app` | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 0. 角色与数据范围(必读) | |
| 10 | + | |
| 11 | +- 判断依据:`CurrentUser.Roles` 中是否存在**忽略大小写**等于 **`admin`** 的角色码(与 JWT 中角色码一致,参见 `AuthSessionAppService` / `ReportsRoleHelper`)。 | |
| 12 | +- **`admin`**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 | |
| 13 | +- **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。 | |
| 14 | +- **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。 | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## 1. Partner / Group / Location 筛选说明 | |
| 19 | + | |
| 20 | +- **`locationId`**:若传则只查该门店(`location.Id` 字符串)。 | |
| 21 | +- **`partnerId`**(`fl_partner.Id`):按合作伙伴名称与 `location.Partner` **文本全等**(trim 后)匹配,得到门店集合再过滤任务。 | |
| 22 | +- **`groupId`**(`fl_group.Id`):按组织的 `GroupName` + 父级 `PartnerName` 与门店的 `GroupName` + `Partner` **文本全等**匹配门店。 | |
| 23 | +- 若 Partner/Group 在库中不存在,返回**空列表/空统计**(不报错)。 | |
| 24 | +- 未传 Partner/Group/Location 时:**不按门店集合预过滤**(仅日期、关键字、用户范围生效)。 | |
| 25 | + | |
| 26 | +--- | |
| 27 | + | |
| 28 | +## 2. Print Log — 分页查询 | |
| 29 | + | |
| 30 | +- **方法**:`GET` | |
| 31 | +- **路径**:`/api/app/reports/print-log-list`(以 Swagger 为准;ABP 约定多为 `get-print-log-list` 映射到该路径) | |
| 32 | + | |
| 33 | +### 2.1 查询参数(`ReportsPrintLogGetListInputVo`) | |
| 34 | + | |
| 35 | +| 参数 | 类型 | 说明 | | |
| 36 | +|------|------|------| | |
| 37 | +| `skipCount` | int | 页码,从 **1** 起 | | |
| 38 | +| `maxResultCount` | int | 每页条数 | | |
| 39 | +| `sorting` | string | 可选:`PrintedAt asc` / `PrintedAt desc`(默认倒序) | | |
| 40 | +| `partnerId` | string | 可选 | | |
| 41 | +| `groupId` | string | 可选 | | |
| 42 | +| `locationId` | string | 可选 | | |
| 43 | +| `startDate` | date | 可选;默认与结束日组成约 30 天窗口 | | |
| 44 | +| `endDate` | date | 可选;默认今天(含当日) | | |
| 45 | +| `keyword` | string | 可选;匹配产品名、标签分类名、产品分类名(模糊) | | |
| 46 | + | |
| 47 | +### 2.2 响应项(`ReportsPrintLogListItemDto`) | |
| 48 | + | |
| 49 | +含:`taskId`、`labelCode`、`productName`、`categoryName`、`templateText`、`printedAt`、`printedByName`、`locationText`、`locationId`(重打必填)、`expiryDateText`(从 `PrintInputJson` 中尝试解析 `expiryDate` / `expiry` / `expirationDate`)。 | |
| 50 | + | |
| 51 | +--- | |
| 52 | + | |
| 53 | +## 3. Print Log — 导出 PDF | |
| 54 | + | |
| 55 | +- **方法**:`GET` | |
| 56 | +- **路径**:`/api/app/reports/export-print-log-pdf` | |
| 57 | +- **查询参数**:与 **§2** 相同(分页字段忽略);最多 **5000** 条,超出返回业务错误。 | |
| 58 | + | |
| 59 | +--- | |
| 60 | + | |
| 61 | +## 4. Print Log — 重打(Reprint) | |
| 62 | + | |
| 63 | +- **方法**:`POST` | |
| 64 | +- **路径**:`/api/app/reports/reprint-print-log`(实现内转发至 `UsAppLabelingAppService.ReprintAsync`,以 Swagger 为准) | |
| 65 | +- **Body**:`UsAppLabelReprintInputVo`:`locationId`、`taskId`、`printQuantity`、`clientRequestId`(可选)、打印机字段(可选)。 | |
| 66 | + | |
| 67 | +--- | |
| 68 | + | |
| 69 | +## 5. Label Report — 统计聚合 | |
| 70 | + | |
| 71 | +- **方法**:`GET` | |
| 72 | +- **路径**:`/api/app/reports/label-report`(以 Swagger 为准) | |
| 73 | + | |
| 74 | +### 5.1 查询参数(`ReportsLabelReportQueryInputVo`) | |
| 75 | + | |
| 76 | +与 Print Log 相同的 `partnerId`、`groupId`、`locationId`、`startDate`、`endDate`、`keyword`(无分页)。 | |
| 77 | + | |
| 78 | +### 5.2 默认时间窗 | |
| 79 | + | |
| 80 | +未传日期时:**结束日 = 今天,开始日 = 结束日前推 29 天(共约 30 个自然日,含首尾)**。 | |
| 81 | + | |
| 82 | +### 5.3 返回(`ReportsLabelReportOutputDto`) | |
| 83 | + | |
| 84 | +- **`summary`**:`totalLabelsPrinted`、上一同长周期 `totalLabelsPrintedPrevPeriod`、`totalLabelsPrintedChangeRate`(%);最热门标签分类名与次数;Top 产品名与次数;`avgDailyPrints` 及环比等。 | |
| 85 | +- **`labelsByCategory`**:按 **标签分类**(`fl_label_category`)汇总当前区间内打印次数。 | |
| 86 | +- **`printVolumeTrend`**:在**当前筛选日期区间内**,取**结束日向前最多 7 个自然日**(与区间求交)的按日打印量。 | |
| 87 | +- **`mostUsedProducts`**:当前区间内产品打印次数 Top20,`usagePercent` 为占**当期总打印次数**的百分比。 | |
| 88 | + | |
| 89 | +--- | |
| 90 | + | |
| 91 | +## 6. Label Report — 导出 PDF | |
| 92 | + | |
| 93 | +- **方法**:`GET` | |
| 94 | +- **路径**:`/api/app/reports/export-label-report-pdf` | |
| 95 | +- **查询参数**:与 **§5** 相同;内容为摘要 + 分类表 + 日趋势表 + Top 产品表。 | |
| 96 | + | |
| 97 | +--- | |
| 98 | + | |
| 99 | +## 7. 数据表与字段依赖 | |
| 100 | + | |
| 101 | +- 打印任务:`fl_label_print_task`(`CreatedBy`、`LocationId`、`PrintedAt`、`PrintInputJson` 等) | |
| 102 | +- 门店:`location`(`Partner`、`GroupName`、`LocationCode`、`LocationName`) | |
| 103 | +- 主数据:`fl_partner`、`fl_group`(筛选用) | |
| 104 | +- 用户:`User`(展示 `PrintedByName`) | |
| 105 | + | |
| 106 | +--- | |
| 107 | + | |
| 108 | +## 8. 前端对接提示 | |
| 109 | + | |
| 110 | +- Print Log 行内 **Reprint**:使用列表返回的 `locationId` + `taskId` 调重打接口。 | |
| 111 | +- 「Export Report」在 Print Log Tab 调 **§3**;在 Label Report Tab 调 **§6**。 | |
| 112 | +- 下拉 **Partner / Group / Location** 与列表筛选字段一致;需保证门店维护的 `Partner` / `GroupName` 与主数据名称一致,否则筛选结果为空。 | ... | ... |
项目相关文档/美国版App登录接口说明.md
| ... | ... | @@ -149,6 +149,142 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
| 149 | 149 | |
| 150 | 150 | --- |
| 151 | 151 | |
| 152 | +## 接口 3:我的资料(My Profile) | |
| 153 | + | |
| 154 | +用于「My Profile」页展示:全名、邮箱、电话、员工号、角色。 | |
| 155 | + | |
| 156 | +### HTTP | |
| 157 | + | |
| 158 | +- **方法**:`GET` | |
| 159 | +- **路径**:`/api/app/us-app-auth/my-profile`(以 Swagger 中 `UsAppAuth` 为准) | |
| 160 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`) | |
| 161 | + | |
| 162 | +### 请求参数 | |
| 163 | + | |
| 164 | +无。 | |
| 165 | + | |
| 166 | +### 响应体(UsAppMyProfileOutputDto) | |
| 167 | + | |
| 168 | +| 字段(JSON) | 类型 | 说明 | | |
| 169 | +|--------------|------|------| | |
| 170 | +| `fullName` | string | `User.Name`,无则 `Nick`,再无则 `UserName` | | |
| 171 | +| `email` | string | `User.Email`;空为 `"无"` | | |
| 172 | +| `phone` | string | 由 `User.Phone`(long)格式化为可读字符串;10 位数字按 `+1 (555) 123-4567` 形式;无则 `"无"` | | |
| 173 | +| `employeeId` | string | 当前取 **`User.UserName`**(可与业务约定为工号/登录名) | | |
| 174 | +| `roleDisplay` | string | 多角色 `Role.RoleName` 按 `OrderNum` 排序后以英文逗号拼接;无角色为 `"无"` | | |
| 175 | +| `primaryRoleCode` | string \| null | 第一个角色的 `RoleCode`,供前端样式 | | |
| 176 | + | |
| 177 | +--- | |
| 178 | + | |
| 179 | +## 接口 4:修改密码(Change Password) | |
| 180 | + | |
| 181 | +与 **`User.JudgePassword` / `BuildPassword`** 一致:校验当前密码后写入新盐值哈希。 | |
| 182 | + | |
| 183 | +### HTTP | |
| 184 | + | |
| 185 | +- **方法**:`POST` | |
| 186 | +- **路径**:`/api/app/us-app-auth/change-password` | |
| 187 | +- **Content-Type**:`application/json` | |
| 188 | +- **鉴权**:需要登录 | |
| 189 | + | |
| 190 | +### 请求体(UsAppChangePasswordInputVo) | |
| 191 | + | |
| 192 | +| 字段(JSON) | 类型 | 必填 | 说明 | | |
| 193 | +|--------------|------|------|------| | |
| 194 | +| `currentPassword` | string | 是 | 当前明文密码 | | |
| 195 | +| `newPassword` | string | 是 | 新密码 | | |
| 196 | +| `confirmNewPassword` | string | 是 | 必须与 `newPassword` **完全一致** | | |
| 197 | + | |
| 198 | +### 新密码复杂度(与原型「Password Requirements」一致) | |
| 199 | + | |
| 200 | +1. 至少 **8** 位 | |
| 201 | +2. 至少 **1** 个大写字母、**1** 个小写字母 | |
| 202 | +3. 至少 **1** 个数字 | |
| 203 | +4. 至少 **1** 个**非字母数字**字符(特殊字符) | |
| 204 | + | |
| 205 | +### 请求示例 | |
| 206 | + | |
| 207 | +```json | |
| 208 | +{ | |
| 209 | + "currentPassword": "OldPass1!", | |
| 210 | + "newPassword": "NewPass9@x", | |
| 211 | + "confirmNewPassword": "NewPass9@x" | |
| 212 | +} | |
| 213 | +``` | |
| 214 | + | |
| 215 | +### 响应 | |
| 216 | + | |
| 217 | +成功时 HTTP 200,无业务体要求(ABP 可能返回空对象);失败为业务异常文案。 | |
| 218 | + | |
| 219 | +### 常见错误 | |
| 220 | + | |
| 221 | +- 未登录:`用户未登录` | |
| 222 | +- 当前密码错误:与登录失败一致(`UserConst.Login_Error` 文案) | |
| 223 | +- 新密码与确认不一致:`新密码与确认密码不一致` | |
| 224 | +- 新密码与当前相同:`新密码不能与当前密码相同` | |
| 225 | +- 不满足复杂度:如 `新密码至少 8 位`、`新密码需包含大写字母` 等 | |
| 226 | + | |
| 227 | +--- | |
| 228 | + | |
| 229 | +## 接口 5:门店详情(Location) | |
| 230 | + | |
| 231 | +用于移动端「Location」页:店名、完整地址、门店电话、营业时间占位、店长姓名与电话。**仅当当前 JWT 用户在 `userlocation` 中绑定该门店**时可查(与接口 1、2 的门店范围一致)。 | |
| 232 | + | |
| 233 | +### HTTP | |
| 234 | + | |
| 235 | +- **方法**:`GET` | |
| 236 | +- **路径**:`/api/app/us-app-auth/location-detail`(以 Swagger 中 `UsAppAuth` → `GetLocationDetail` 为准;`locationId` 一般为 **Query** 参数) | |
| 237 | +- **鉴权**:需要登录(`Authorization: Bearer {token}`) | |
| 238 | + | |
| 239 | +### 请求参数 | |
| 240 | + | |
| 241 | +| 参数名 | 位置 | 类型 | 必填 | 说明 | | |
| 242 | +|--------|------|------|------|------| | |
| 243 | +| `locationId` | Query | string | 是 | 门店主键,Guid 字符串(与 `locations[].id` 一致) | | |
| 244 | + | |
| 245 | +### 传参示例 | |
| 246 | + | |
| 247 | +```http | |
| 248 | +GET /api/app/us-app-auth/location-detail?locationId=a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1 | |
| 249 | +Host: localhost:19001 | |
| 250 | +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | |
| 251 | +``` | |
| 252 | + | |
| 253 | +### 响应体(UsAppLocationDetailOutputDto) | |
| 254 | + | |
| 255 | +| 字段(JSON) | 类型 | 说明 | | |
| 256 | +|--------------|------|------| | |
| 257 | +| `locationId` | string | 门店主键 | | |
| 258 | +| `locationName` | string | 门店名称;空为 `"无"` | | |
| 259 | +| `fullAddress` | string | 与登录接口相同的地址拼接规则;无有效片段为 `"无"` | | |
| 260 | +| `storePhone` | string | `location.Phone` 去首尾空格;空为 `"无"` | | |
| 261 | +| `operatingHours` | string | 当前 **`location` 表无营业时间字段**,固定返回 **`"无"`**(后续若落库可再对接) | | |
| 262 | +| `managerName` | string | 在同店 `userlocation` 绑定用户中,取角色 `RoleCode` / `RoleName`(忽略大小写)包含 **`manager`** 的第一人展示姓名(`Name` → `Nick` → `UserName`);无匹配为 **`"无"`** | | |
| 263 | +| `managerPhone` | string | 同上用户的 `User.Phone`(long)按与「我的资料」相同的可读格式;无电话为 **`"无"`** | | |
| 264 | + | |
| 265 | +### 响应示例 | |
| 266 | + | |
| 267 | +```json | |
| 268 | +{ | |
| 269 | + "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | |
| 270 | + "locationName": "Downtown Store", | |
| 271 | + "fullAddress": "123 Main St, New York, NY 10001", | |
| 272 | + "storePhone": "(212) 555-0100", | |
| 273 | + "operatingHours": "无", | |
| 274 | + "managerName": "Jane Doe", | |
| 275 | + "managerPhone": "+1 (555) 123-4567" | |
| 276 | +} | |
| 277 | +``` | |
| 278 | + | |
| 279 | +### 常见错误(业务异常文案) | |
| 280 | + | |
| 281 | +- 未登录:`用户未登录` | |
| 282 | +- `locationId` 非法或空:`无效的门店标识` | |
| 283 | +- 当前用户未绑定该门店:`当前账号未绑定该门店,无法查看` | |
| 284 | +- 门店已删或不存在:`门店不存在或已删除` | |
| 285 | + | |
| 286 | +--- | |
| 287 | + | |
| 152 | 288 | ## 与其他登录方式的区别 |
| 153 | 289 | |
| 154 | 290 | | 场景 | 说明 | | ... | ... |