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