diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs
index 08e7440..87581eb 100644
--- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs
@@ -1,3 +1,5 @@
+using System.Text.Json;
+
namespace FoodLabeling.Application.Helpers;
///
@@ -45,11 +47,37 @@ public static class RbacAccessPermissionHelper
}
///
- /// 解析 accessPermissions 入参(英文逗号分隔,忽略大小写)。
+ /// 解析 accessPermissions 入参:支持 JSON 数组字符串(如 ["manage_labels"])、英文逗号分隔。
///
public static List ParseAccessPermissionCodes(string accessPermissions)
{
- return accessPermissions
+ var raw = accessPermissions?.Trim();
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ return new List();
+ }
+
+ if (raw.StartsWith('['))
+ {
+ try
+ {
+ var fromJson = JsonSerializer.Deserialize>(raw);
+ if (fromJson is { Count: > 0 })
+ {
+ return fromJson
+ .Where(c => !string.IsNullOrWhiteSpace(c))
+ .Select(c => c.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+ }
+ catch
+ {
+ // fall through to delimiter split
+ }
+ }
+
+ return raw
.Split(new[] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(c => !string.IsNullOrWhiteSpace(c))
.Select(c => c.Trim())
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
index a3a6de6..54d1856 100644
--- 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
@@ -8,7 +8,9 @@ public class DashboardRecentLabelItemDto
/// 打印任务 Id(fl_label_print_task.Id)
public string TaskId { get; set; } = string.Empty;
- /// 标签编码(界面 Serial / Label ID,如 1-251201)
+ ///
+ /// 展示用 Label ID:门店当日打印序号 yyyyMMdd-n(与 preview / print-log 一致;非 fl_label.LabelCode)
+ ///
public string LabelCode { get; set; } = string.Empty;
/// 展示名称:优先产品名,否则标签名称
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs
index 731e532..796ff67 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs
@@ -13,7 +13,12 @@ public class LabelTemplateGetListInputVo : PagedAndSortedResultRequestDto
public string? Keyword { get; set; }
///
- /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的模板,以及 appliedLocation=ALL 的模板
+ /// 按 Company 筛选(fl_partner.Id);与 、 按「门店优先」解析
+ ///
+ public string? PartnerId { get; set; }
+
+ ///
+ /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的模板,以及全范围模板
///
public string? GroupId { get; set; }
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs
index 41d380a..41bf2f0 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs
@@ -29,7 +29,7 @@ public class RbacRoleCreateInputVo
public List? MenuIds { get; set; }
///
- /// 按 PermissionCode 绑定菜单(英文逗号分隔);传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时)
+ /// 按 PermissionCode 绑定菜单:JSON 数组字符串(如 ["manage_labels"])、英文逗号分隔;传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时)
///
public string? AccessPermissions { get; set; }
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs
index b443b71..646bc84 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs
@@ -28,7 +28,7 @@ public class TeamMemberCreateInputVo
public List? PartnerIds { get; set; }
///
- /// 适用 Region 多选(fl_group.Id);与 合并
+ /// 适用 Region 多选(fl_group.Id);Company Admin 仅传 Company 时可省略,后端自动绑定该公司下全部 Region 与门店
///
public List? RegionIds { get; set; }
@@ -38,7 +38,7 @@ public class TeamMemberCreateInputVo
public List? GroupIds { get; set; }
///
- /// 适用门店多选(location.Id);与 Company/Region 合并后写入 userlocation
+ /// 适用门店多选(location.Id);Company Admin 仅传 Company 时可省略,与 Region 合并后写入 userlocation
///
public List? LocationIds { get; set; }
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs
index e28a30a..29169e3 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs
@@ -27,6 +27,9 @@ public class TeamMemberGetListOutputDto
/// 适用 Region Id 列表(多选)
public List RegionIds { get; set; } = new();
+ /// 已绑定门店 Id 列表(location.Id)
+ public List LocationIds { get; set; } = new();
+
public List AssignedLocations { get; set; } = new();
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs
index 98299e9..163b529 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs
@@ -28,7 +28,7 @@ public class TeamMemberUpdateInputVo
public List? PartnerIds { get; set; }
///
- /// 适用 Region 多选(fl_group.Id);与 合并
+ /// 适用 Region 多选(fl_group.Id);Company Admin 仅传 Company 时可省略
///
public List? RegionIds { get; set; }
@@ -38,7 +38,7 @@ public class TeamMemberUpdateInputVo
public List? GroupIds { get; set; }
///
- /// 适用门店多选(location.Id);与 Company/Region 合并后写入 userlocation
+ /// 适用门店多选(location.Id);Company Admin 仅传 Company 时可省略
///
public List? LocationIds { get; set; }
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeCompanyOptionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeCompanyOptionDto.cs
deleted file mode 100644
index 0ad3416..0000000
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeCompanyOptionDto.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
-
-/// auth-scope / us-app-auth 公司下拉项
-public class AuthScopeCompanyOptionDto
-{
- public string Id { get; set; } = string.Empty;
-
- public string PartnerName { 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/UsAppAuth/AuthScopeLocationOptionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeLocationOptionDto.cs
deleted file mode 100644
index 878d9cf..0000000
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeLocationOptionDto.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
-
-/// auth-scope / us-app-auth 门店下拉项
-public class AuthScopeLocationOptionDto
-{
- public string Id { get; set; } = string.Empty;
-
- public string LocationCode { get; set; } = string.Empty;
-
- public string LocationName { get; set; } = string.Empty;
-
- public string FullAddress { get; set; } = string.Empty;
-
- public bool State { get; set; } = true;
-
- public string PartnerId { get; set; } = string.Empty;
-
- public string GroupId { get; set; } = string.Empty;
-
- public string GroupName { get; set; } = string.Empty;
-}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeRegionOptionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeRegionOptionDto.cs
deleted file mode 100644
index dac6d8e..0000000
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeRegionOptionDto.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
-
-/// auth-scope / us-app-auth Region 下拉项
-public class AuthScopeRegionOptionDto
-{
- public string Id { get; set; } = string.Empty;
-
- 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/UsAppAuth/AuthScopeSelectLocationOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeSelectLocationOutputDto.cs
deleted file mode 100644
index cc38312..0000000
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeSelectLocationOutputDto.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
-
-/// 确认选店返回
-public class AuthScopeSelectLocationOutputDto
-{
- public string PartnerId { get; set; } = string.Empty;
-
- public string PartnerName { get; set; } = string.Empty;
-
- public string GroupId { get; set; } = string.Empty;
-
- public string GroupName { get; set; } = string.Empty;
-
- public UsAppBoundLocationDto Location { get; set; } = new();
-}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs
index f819a65..20fe018 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs
@@ -11,5 +11,15 @@ public class PrintLogGetListInputVo : PagedAndSortedResultRequestDto
/// 当前门店 Id(location.Id,Guid 字符串)
///
public string LocationId { get; set; } = string.Empty;
-}
+ ///
+ /// 打印日期(自然日);按 PrintedAt ?? CreationTime 筛选该日记录。
+ /// 未传且未传 时不按日过滤(返回该门店全部打印记录,分页)。
+ ///
+ public DateTime? PrintDate { get; set; }
+
+ ///
+ /// 打印日期字符串(yyyy-MM-dd);优先于 ,避免 JSON 仅日期时的 UTC 歧义。
+ ///
+ public string? PrintDateDay { get; set; }
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs
index c55e7d3..e4ca62f 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs
@@ -14,6 +14,7 @@ public interface IDashboardAppService : IApplicationService
///
/// 按 Token 数据范围统计:系统管理员为全平台;其它账号仅统计其 userlocation 绑定门店所属公司范围内数据。
/// activeUsers / people 仅统计有门店绑定的成员,且不包含内置系统 admin(用户名 admin 或角色码 admin)。
+ /// recentLabels[].labelCode 为门店当日打印序号 yyyyMMdd-n,规则与 POST /api/app/us-app-labeling/preview、print-log 一致。
///
Task GetOverviewAsync();
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs
index 3938824..8b015d3 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs
@@ -11,8 +11,9 @@ namespace FoodLabeling.Application.Contracts.IServices;
public interface ILabelTemplateAppService : IApplicationService
{
///
- /// 标签模板分页列表;Query 支持 groupId(Region)、locationId(门店)筛选;
- /// 出参含 region/location、items/itemNames(模板控件名称,逗号拼接)。
+ /// 标签模板分页列表;Query 支持 partnerId(Company)、groupId(Region)、locationId(门店)筛选;
+ /// 出参含 company/region/location、items/itemNames(模板控件名称,逗号拼接)。
+ /// SkipCount 为页码(从 1 起,第一页传 1)。
///
Task> GetListAsync(LabelTemplateGetListInputVo 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 3de8e70..e49108e 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
@@ -92,16 +92,4 @@ public interface IUsAppAuthAppService : IApplicationService
/// Invalid code or password policy.
/// Server error
Task PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input);
-
- /// App 管理员:可选公司列表
- Task> GetAdminScopeCompaniesAsync();
-
- /// App 管理员:指定公司下 Region
- Task> GetAdminScopeRegionsAsync(string partnerId);
-
- /// App 管理员:指定公司与 Region 下门店
- Task> GetAdminScopeLocationsAsync(string partnerId, string groupId);
-
- /// App 管理员:确认当前工作门店
- Task SelectAdminScopeLocationAsync(UsAppSelectAdminScopeLocationInputVo input);
}
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 3a07f83..7c86d90 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
@@ -33,7 +33,7 @@ public interface IUsAppLabelingAppService : IApplicationService
///
/// App 打印日志:当前门店打印记录(分页)。管理员 / Partner 角色可见门店内全部;其它角色仅本人。
- /// 出参 labelId 为门店当日打印序号(yyyyMMdd-n);labelEntityId 为 fl_label.Id;labelCode 为标签编码。
+ /// 支持 printDate 按自然日筛选;出参 labelId 为门店当日打印序号(yyyyMMdd-n)。
///
Task> GetPrintLogListAsync(PrintLogGetListInputVo input);
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs
index c637bc0..ec0cf8c 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs
@@ -29,7 +29,8 @@ public static class LabelTemplateListItemsHelper
ElementName = x.ElementName,
TypeAdd = x.TypeAdd,
ElementType = x.ElementType,
- OrderNum = x.OrderNum
+ OrderNum = x.OrderNum,
+ ElementKey = x.ElementKey
})
.ToListAsync();
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateQueryHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateQueryHelper.cs
new file mode 100644
index 0000000..1acd3ea
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateQueryHelper.cs
@@ -0,0 +1,37 @@
+using FoodLabeling.Application.Services.DbModels;
+using SqlSugar;
+
+namespace FoodLabeling.Application.Helpers;
+
+///
+/// 标签模板查询列投影:仅映射库内真实列,避免 ORM 元数据缓存或旧实体仍 SELECT 不存在的 scope 列。
+///
+public static class LabelTemplateQueryHelper
+{
+ ///
+ /// 列表/详情/引用校验等只读场景:强制 SQL 不含 AppliedPartnerType / AppliedRegionType。
+ ///
+ public static ISugarQueryable ProjectListColumns(
+ ISugarQueryable query) =>
+ query.Select(x => new FlLabelTemplateDbEntity
+ {
+ Id = x.Id,
+ IsDeleted = x.IsDeleted,
+ CreationTime = x.CreationTime,
+ CreatorId = x.CreatorId,
+ LastModifierId = x.LastModifierId,
+ LastModificationTime = x.LastModificationTime,
+ ConcurrencyStamp = x.ConcurrencyStamp,
+ TemplateCode = x.TemplateCode,
+ TemplateName = x.TemplateName,
+ LabelType = x.LabelType,
+ Unit = x.Unit,
+ Width = x.Width,
+ Height = x.Height,
+ AppliedLocationType = x.AppliedLocationType,
+ ShowRuler = x.ShowRuler,
+ ShowGrid = x.ShowGrid,
+ VersionNo = x.VersionNo,
+ State = x.State
+ });
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs
index 85cdb40..30532ec 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs
@@ -150,17 +150,24 @@ public static class LabelTemplateScopeHelper
string? currentUserId,
DateTime now)
{
- await db.Deleteable()
- .Where(x => x.TemplateId == templateId)
- .ExecuteCommandAsync();
- await db.Deleteable()
- .Where(x => x.TemplateId == templateId)
- .ExecuteCommandAsync();
+ var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db);
+
+ if (hasExtendedScope)
+ {
+ await db.Deleteable()
+ .Where(x => x.TemplateId == templateId)
+ .ExecuteCommandAsync();
+ await db.Deleteable()
+ .Where(x => x.TemplateId == templateId)
+ .ExecuteCommandAsync();
+ }
+
await db.Deleteable()
.Where(x => x.TemplateId == templateId)
.ExecuteCommandAsync();
- if (string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
+ if (hasExtendedScope
+ && string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
&& scope.PartnerIds.Count > 0)
{
var rows = scope.PartnerIds.Select(pid => new FlLabelTemplatePartnerDbEntity
@@ -174,7 +181,8 @@ public static class LabelTemplateScopeHelper
await db.Insertable(rows).ExecuteCommandAsync();
}
- if (string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
+ if (hasExtendedScope
+ && string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
&& scope.RegionIds.Count > 0)
{
var rows = scope.RegionIds.Select(gid => new FlLabelTemplateRegionDbEntity
@@ -216,12 +224,18 @@ public static class LabelTemplateScopeHelper
var templateIds = templates.Select(x => x.Id).Distinct(StringComparer.Ordinal).ToList();
- var partnerLinks = await db.Queryable()
- .Where(x => templateIds.Contains(x.TemplateId))
- .ToListAsync();
- var regionLinks = await db.Queryable()
- .Where(x => templateIds.Contains(x.TemplateId))
- .ToListAsync();
+ var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db);
+
+ var partnerLinks = hasExtendedScope
+ ? await db.Queryable()
+ .Where(x => templateIds.Contains(x.TemplateId))
+ .ToListAsync()
+ : new List();
+ var regionLinks = hasExtendedScope
+ ? await db.Queryable()
+ .Where(x => templateIds.Contains(x.TemplateId))
+ .ToListAsync()
+ : new List();
var locationLinks = await db.Queryable()
.Where(x => templateIds.Contains(x.TemplateId))
.ToListAsync();
@@ -274,8 +288,6 @@ public static class LabelTemplateScopeHelper
foreach (var template in templates)
{
- var partnerType = NormalizeScopeType(template.AppliedPartnerType, ScopeAll);
- var regionType = NormalizeScopeType(template.AppliedRegionType, ScopeAll);
var locationType = NormalizeScopeType(template.AppliedLocationType, ScopeAll);
var pIds = LocationScopeBindingHelper.NormalizeIds(
@@ -285,34 +297,39 @@ public static class LabelTemplateScopeHelper
var lIds = LocationScopeBindingHelper.NormalizeIds(
locationLinks.Where(x => x.TemplateId == template.Id).Select(x => x.LocationId).ToList());
- if (pIds.Count == 0
- && string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
+ var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll;
+ var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll;
+
+ if (hasExtendedScope
+ && pIds.Count == 0
&& lIds.Count > 0)
{
pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds);
+ if (pIds.Count > 0)
+ {
+ partnerType = ScopeSpecified;
+ }
}
- if (rIds.Count == 0
- && string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
+ if (hasExtendedScope
+ && rIds.Count == 0
&& lIds.Count > 0)
{
rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds);
- }
-
- if (pIds.Count == 0 && !string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))
- {
- partnerType = ScopeAll;
- }
-
- if (rIds.Count == 0 && !string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))
- {
- regionType = ScopeAll;
+ if (rIds.Count > 0)
+ {
+ regionType = ScopeSpecified;
+ }
}
if (lIds.Count == 0 && !string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))
{
locationType = ScopeAll;
}
+ else if (lIds.Count > 0)
+ {
+ locationType = ScopeSpecified;
+ }
var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase)
? AllCompaniesDisplay
@@ -354,12 +371,36 @@ public static class LabelTemplateScopeHelper
return query;
}
+ var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db);
+
if (scopedLocationIds.Count == 0)
{
+ return hasExtendedScope
+ ? query.Where(t =>
+ t.AppliedLocationType == ScopeAll
+ && !SqlFunc.Subqueryable()
+ .Where(l => l.TemplateId == t.Id)
+ .Any()
+ && !SqlFunc.Subqueryable()
+ .Where(p => p.TemplateId == t.Id)
+ .Any()
+ && !SqlFunc.Subqueryable()
+ .Where(r => r.TemplateId == t.Id)
+ .Any())
+ : query.Where(t =>
+ t.AppliedLocationType == ScopeAll
+ && !SqlFunc.Subqueryable()
+ .Where(l => l.TemplateId == t.Id)
+ .Any());
+ }
+
+ if (!hasExtendedScope)
+ {
return query.Where(t =>
- t.AppliedPartnerType == ScopeAll
- && t.AppliedRegionType == ScopeAll
- && t.AppliedLocationType == ScopeAll);
+ t.AppliedLocationType == ScopeAll
+ || SqlFunc.Subqueryable()
+ .Where(l => l.TemplateId == t.Id && scopedLocationIds.Contains(l.LocationId))
+ .Any());
}
var scopedPartnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(
@@ -368,13 +409,28 @@ public static class LabelTemplateScopeHelper
db, scopedLocationIds);
return query.Where(t =>
- (t.AppliedPartnerType == ScopeAll && t.AppliedRegionType == ScopeAll && t.AppliedLocationType == ScopeAll)
+ (
+ t.AppliedLocationType == ScopeAll
+ && !SqlFunc.Subqueryable()
+ .Where(l => l.TemplateId == t.Id)
+ .Any()
+ && !SqlFunc.Subqueryable()
+ .Where(p => p.TemplateId == t.Id)
+ .Any()
+ && !SqlFunc.Subqueryable()
+ .Where(r => r.TemplateId == t.Id)
+ .Any()
+ )
|| (
- (t.AppliedPartnerType == ScopeAll
+ (!SqlFunc.Subqueryable()
+ .Where(p => p.TemplateId == t.Id)
+ .Any()
|| SqlFunc.Subqueryable()
.Where(p => p.TemplateId == t.Id && scopedPartnerIds.Contains(p.PartnerId))
.Any())
- && (t.AppliedRegionType == ScopeAll
+ && (!SqlFunc.Subqueryable()
+ .Where(r => r.TemplateId == t.Id)
+ .Any()
|| SqlFunc.Subqueryable()
.Where(r => r.TemplateId == t.Id && scopedGroupIds.Contains(r.GroupId))
.Any())
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs
new file mode 100644
index 0000000..22c2998
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs
@@ -0,0 +1,156 @@
+using SqlSugar;
+
+namespace FoodLabeling.Application.Helpers;
+
+///
+/// 探测标签模板 Company/Region scope 列与关联表是否已迁移(fl_label_template_scope.sql)。
+/// 未迁移时 ORM 实体不含 AppliedPartnerType/AppliedRegionType,读写走 raw SQL 或关联表推断。
+///
+public static class LabelTemplateScopeSchemaHelper
+{
+ private static LabelTemplateScopeSchemaStatus? _cached;
+
+ public sealed class LabelTemplateScopeSchemaStatus
+ {
+ public bool HasAppliedPartnerTypeColumn { get; init; }
+
+ public bool HasAppliedRegionTypeColumn { get; init; }
+
+ public bool HasPartnerScopeTable { get; init; }
+
+ public bool HasRegionScopeTable { get; init; }
+
+ /// partner/region 关联表均已创建(用于多选明细)。
+ public bool HasPartnerRegionScopeTables => HasPartnerScopeTable && HasRegionScopeTable;
+
+ /// 列 + 关联表均已迁移。
+ public bool IsFullSchema =>
+ HasAppliedPartnerTypeColumn
+ && HasAppliedRegionTypeColumn
+ && HasPartnerRegionScopeTables;
+ }
+
+ public static async Task GetStatusAsync(ISqlSugarClient db)
+ {
+ if (_cached is not null)
+ {
+ return _cached;
+ }
+
+ try
+ {
+ var partnerColCount = await db.Ado.GetIntAsync(
+ """
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'fl_label_template'
+ AND COLUMN_NAME = 'AppliedPartnerType'
+ """);
+
+ var regionColCount = await db.Ado.GetIntAsync(
+ """
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'fl_label_template'
+ AND COLUMN_NAME = 'AppliedRegionType'
+ """);
+
+ var partnerTableCount = await db.Ado.GetIntAsync(
+ """
+ SELECT COUNT(*)
+ FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'fl_label_template_partner'
+ """);
+
+ var regionTableCount = await db.Ado.GetIntAsync(
+ """
+ SELECT COUNT(*)
+ FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'fl_label_template_region'
+ """);
+
+ _cached = new LabelTemplateScopeSchemaStatus
+ {
+ HasAppliedPartnerTypeColumn = partnerColCount > 0,
+ HasAppliedRegionTypeColumn = regionColCount > 0,
+ HasPartnerScopeTable = partnerTableCount > 0,
+ HasRegionScopeTable = regionTableCount > 0
+ };
+ }
+ catch
+ {
+ _cached = new LabelTemplateScopeSchemaStatus();
+ }
+
+ return _cached;
+ }
+
+ /// 单元测试或切换库后调用。
+ public static void ResetCacheForTests() => _cached = null;
+
+ ///
+ /// 是否已创建 fl_label_template_partner 与 fl_label_template_region。
+ ///
+ public static async Task HasPartnerRegionScopeTablesAsync(ISqlSugarClient db)
+ {
+ var status = await GetStatusAsync(db);
+ return status.HasPartnerRegionScopeTables;
+ }
+
+ ///
+ /// 已迁移 scope 列时,写入 AppliedPartnerType / AppliedRegionType(未迁移则 no-op)。
+ ///
+ public static async Task SetAppliedScopeTypesAsync(
+ ISqlSugarClient db,
+ string templateId,
+ string appliedPartnerType,
+ string appliedRegionType)
+ {
+ if (string.IsNullOrWhiteSpace(templateId))
+ {
+ return;
+ }
+
+ var status = await GetStatusAsync(db);
+ if (!status.HasAppliedPartnerTypeColumn && !status.HasAppliedRegionTypeColumn)
+ {
+ return;
+ }
+
+ if (status.HasAppliedPartnerTypeColumn && status.HasAppliedRegionTypeColumn)
+ {
+ await db.Ado.ExecuteCommandAsync(
+ """
+ UPDATE fl_label_template
+ SET AppliedPartnerType = @partnerType,
+ AppliedRegionType = @regionType
+ WHERE Id = @id
+ """,
+ new
+ {
+ partnerType = appliedPartnerType.Trim(),
+ regionType = appliedRegionType.Trim(),
+ id = templateId.Trim()
+ });
+ return;
+ }
+
+ if (status.HasAppliedPartnerTypeColumn)
+ {
+ await db.Ado.ExecuteCommandAsync(
+ "UPDATE fl_label_template SET AppliedPartnerType = @type WHERE Id = @id",
+ new { type = appliedPartnerType.Trim(), id = templateId.Trim() });
+ }
+
+ if (status.HasAppliedRegionTypeColumn)
+ {
+ await db.Ado.ExecuteCommandAsync(
+ "UPDATE fl_label_template SET AppliedRegionType = @type WHERE Id = @id",
+ new { type = appliedRegionType.Trim(), id = templateId.Trim() });
+ }
+ }
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
index abb1491..76d9b60 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
@@ -2,6 +2,8 @@ using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
using SqlSugar;
using Volo.Abp;
+using Volo.Abp.Users;
+using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace FoodLabeling.Application.Helpers;
@@ -12,6 +14,87 @@ namespace FoodLabeling.Application.Helpers;
public static class LocationScopeBindingHelper
{
///
+ /// 读取用户 userlocation 绑定的门店 Id(Guid 归一化,避免大小写导致漏绑)。
+ ///
+ public static async Task> ResolveBoundLocationIdsForUserAsync(
+ ISqlSugarClient db,
+ Guid userId)
+ {
+ var userKey = TeamMemberListScopeHelper.UserKey(userId);
+ var links = await db.Queryable()
+ .Where(x => !x.IsDeleted)
+ .ToListAsync();
+
+ var locationIds = new HashSet(StringComparer.Ordinal);
+ foreach (var link in links)
+ {
+ if (TeamMemberListScopeHelper.NormalizeScopeKey(link.UserId) != userKey)
+ {
+ continue;
+ }
+
+ var locKey = TeamMemberListScopeHelper.NormalizeScopeKey(link.LocationId);
+ if (string.IsNullOrEmpty(locKey))
+ {
+ continue;
+ }
+
+ locationIds.Add(locKey);
+ }
+
+ return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList();
+ }
+
+ ///
+ /// 与 Team Member 详情/编辑回显一致:Company Admin 展示绑定 Company 下全部 Region,其它角色按门店反推 Region。
+ ///
+ public static async Task> ResolveDisplayRegionIdsForUserAsync(
+ ISqlSugarClient db,
+ Guid userId,
+ Guid? roleId)
+ {
+ var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId);
+ if (locationIds.Count == 0)
+ {
+ return new List();
+ }
+
+ var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds);
+ var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds);
+
+ if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0)
+ {
+ var allRegions = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds);
+ if (allRegions.Count > 0)
+ {
+ regionIds = allRegions;
+ }
+ }
+
+ return regionIds;
+ }
+
+ ///
+ /// 当前登录用户可见 Region Id(fl_group.Id),规则同 。
+ ///
+ public static async Task> ResolveDisplayRegionIdsForCurrentUserAsync(
+ ISqlSugarClient db,
+ ICurrentUser currentUser)
+ {
+ if (currentUser.Id is null)
+ {
+ return new List();
+ }
+
+ var roleId = await db.Queryable()
+ .Where(ur => ur.UserId == currentUser.Id.Value)
+ .Select(ur => (Guid?)ur.RoleId)
+ .FirstAsync();
+
+ return await ResolveDisplayRegionIdsForUserAsync(db, currentUser.Id.Value, roleId);
+ }
+
+ ///
/// 列表筛选:门店 Id 优先,否则 Region(groupId),否则 Company(partnerId);均未传返回 null。
///
public static async Task?> ResolveFilteredLocationIdsForListAsync(
@@ -55,7 +138,12 @@ public static class LocationScopeBindingHelper
var gName = g.GroupName?.Trim() ?? string.Empty;
var partner = await db.Queryable()
.FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId);
- var pName = partner?.PartnerName?.Trim() ?? string.Empty;
+ if (partner is null)
+ {
+ return new List();
+ }
+
+ var pName = partner.PartnerName?.Trim() ?? string.Empty;
q = q.Where(x => x.GroupName == gName && x.Partner == pName);
}
else if (!string.IsNullOrWhiteSpace(pid))
@@ -358,6 +446,35 @@ public static class LocationScopeBindingHelper
}
///
+ /// 解析 Company(fl_partner.Id)下全部 Region Id(fl_group.Id)。
+ ///
+ public static async Task> ResolveGroupIdsFromPartnerIdsAsync(
+ ISqlSugarClient db,
+ IReadOnlyList? partnerIds)
+ {
+ var ids = NormalizeIds(partnerIds);
+ if (ids.Count == 0)
+ {
+ return new List();
+ }
+
+ var result = new HashSet(StringComparer.Ordinal);
+ foreach (var pid in ids)
+ {
+ var groupIds = await db.Queryable()
+ .Where(x => !x.IsDeleted && x.PartnerId == pid)
+ .Select(x => x.Id)
+ .ToListAsync();
+ foreach (var gid in groupIds.Where(x => !string.IsNullOrWhiteSpace(x)))
+ {
+ result.Add(gid.Trim());
+ }
+ }
+
+ return result.OrderBy(x => x, StringComparer.Ordinal).ToList();
+ }
+
+ ///
/// 校验合并后的门店 Id 均存在。
///
public static async Task ValidateLocationIdsExistAsync(ISqlSugarClient db, IReadOnlyList locationIds)
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs
index 67d04a9..32cb96f 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs
@@ -39,37 +39,16 @@ public static class PartnerScopeHelper
return new PartnerScopeFilter();
}
- var userId = currentUser.Id.Value.ToString();
- var locationIds = await dbContext.SqlSugarClient.Queryable()
- .Where(x => !x.IsDeleted && x.UserId == userId)
- .Select(x => x.LocationId)
- .Distinct()
- .ToListAsync();
-
+ var db = dbContext.SqlSugarClient;
+ var locationIds = await LocationScopeBindingHelper.ResolveBoundLocationIdsForUserAsync(
+ db, currentUser.Id.Value);
if (locationIds.Count == 0)
{
return new PartnerScopeFilter();
}
- var partnerKeys = await dbContext.SqlSugarClient.Queryable()
- .Where(x => !x.IsDeleted && locationIds.Contains(x.Id.ToString()))
- .Where(x => x.Partner != null && x.Partner != string.Empty)
- .Select(x => x.Partner!)
- .Distinct()
- .ToListAsync();
-
- if (partnerKeys.Count == 0)
- {
- return new PartnerScopeFilter();
- }
-
- var allowedIds = await dbContext.SqlSugarClient.Queryable()
- .Where(x => !x.IsDeleted)
- .Where(x => partnerKeys.Contains(x.PartnerName) || partnerKeys.Contains(x.Id))
- .Select(x => x.Id)
- .Distinct()
- .ToListAsync();
-
+ var allowedIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(
+ db, locationIds);
return new PartnerScopeFilter { AllowedPartnerIds = allowedIds };
}
@@ -95,7 +74,8 @@ public static class PartnerScopeHelper
}
///
- /// 组织(fl_group)数据范围:管理员可见全部;非管理员仅可见其 userlocation 绑定门店对应的 Region(公司 + 组织名与 location 一致)。
+ /// 组织(fl_group)数据范围:管理员可见全部;Company Admin 可见绑定公司下全部 Region(与 Team Member 详情 regionIds 一致);
+ /// 其它非管理员仅可见其绑定门店反推的 Region。
///
public sealed class GroupScopeFilter
{
@@ -105,7 +85,7 @@ public static class PartnerScopeHelper
}
///
- /// 解析当前登录用户可查询的组织 Id 范围(与 Reports 按 location.Partner + location.GroupName 对齐)。
+ /// 解析当前登录用户可查询的组织 Id 范围(与 Team Member 编辑回显 regionIds/groupIds 对齐)。
///
public static async Task ResolveGroupScopeAsync(
ICurrentUser currentUser,
@@ -121,62 +101,10 @@ public static class PartnerScopeHelper
return new GroupScopeFilter();
}
- var userId = currentUser.Id.Value.ToString();
- var locationRows = await dbContext.SqlSugarClient.Queryable()
- .InnerJoin((ul, loc) =>
- !loc.IsDeleted && ul.LocationId == loc.Id.ToString())
- .Where(ul => !ul.IsDeleted && ul.UserId == userId)
- .Select((ul, loc) => new { loc.Partner, loc.GroupName })
- .ToListAsync();
-
- if (locationRows.Count == 0)
- {
- return new GroupScopeFilter();
- }
+ var regionIds = await LocationScopeBindingHelper.ResolveDisplayRegionIdsForCurrentUserAsync(
+ dbContext.SqlSugarClient, currentUser);
- var pairs = locationRows
- .Where(x => !string.IsNullOrWhiteSpace(x.GroupName))
- .Select(x => (Partner: (x.Partner ?? string.Empty).Trim(), GroupName: x.GroupName!.Trim()))
- .Where(x => !string.IsNullOrEmpty(x.GroupName) && !string.IsNullOrEmpty(x.Partner))
- .Distinct()
- .ToList();
-
- if (pairs.Count == 0)
- {
- return new GroupScopeFilter();
- }
-
- var partnerKeys = pairs.Select(x => x.Partner).Distinct().ToList();
- var partners = await dbContext.SqlSugarClient.Queryable()
- .Where(x => !x.IsDeleted)
- .Where(x => partnerKeys.Contains(x.PartnerName) || partnerKeys.Contains(x.Id))
- .Select(x => new { x.Id, x.PartnerName })
- .ToListAsync();
-
- var allowedGroupIds = new HashSet(StringComparer.Ordinal);
- foreach (var pair in pairs)
- {
- var partnerId = partners
- .FirstOrDefault(p =>
- string.Equals(p.PartnerName, pair.Partner, StringComparison.Ordinal) ||
- string.Equals(p.Id, pair.Partner, StringComparison.Ordinal))
- ?.Id;
- if (string.IsNullOrEmpty(partnerId))
- {
- continue;
- }
-
- var ids = await dbContext.SqlSugarClient.Queryable()
- .Where(g => !g.IsDeleted && g.PartnerId == partnerId && g.GroupName == pair.GroupName)
- .Select(g => g.Id)
- .ToListAsync();
- foreach (var id in ids)
- {
- allowedGroupIds.Add(id);
- }
- }
-
- return new GroupScopeFilter { AllowedGroupIds = allowedGroupIds.ToList() };
+ return new GroupScopeFilter { AllowedGroupIds = regionIds };
}
///
@@ -191,12 +119,13 @@ public static class PartnerScopeHelper
return query;
}
- var ids = scope.AllowedGroupIds;
- if (ids.Count == 0)
+ var allowedGuids = TeamMemberListScopeHelper.ParseGuidHashSet(scope.AllowedGroupIds);
+ if (allowedGuids.Count == 0)
{
return query.Where((g, p) => false);
}
- return query.Where((g, p) => ids.Contains(g.Id));
+ var guidList = allowedGuids.ToList();
+ return query.Where((g, p) => guidList.Contains(SqlFunc.ToGuid(g.Id)));
}
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs
index 08e7440..87581eb 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs
@@ -1,3 +1,5 @@
+using System.Text.Json;
+
namespace FoodLabeling.Application.Helpers;
///
@@ -45,11 +47,37 @@ public static class RbacAccessPermissionHelper
}
///
- /// 解析 accessPermissions 入参(英文逗号分隔,忽略大小写)。
+ /// 解析 accessPermissions 入参:支持 JSON 数组字符串(如 ["manage_labels"])、英文逗号分隔。
///
public static List ParseAccessPermissionCodes(string accessPermissions)
{
- return accessPermissions
+ var raw = accessPermissions?.Trim();
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ return new List();
+ }
+
+ if (raw.StartsWith('['))
+ {
+ try
+ {
+ var fromJson = JsonSerializer.Deserialize>(raw);
+ if (fromJson is { Count: > 0 })
+ {
+ return fromJson
+ .Where(c => !string.IsNullOrWhiteSpace(c))
+ .Select(c => c.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+ }
+ catch
+ {
+ // fall through to delimiter split
+ }
+ }
+
+ return raw
.Split(new[] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(c => !string.IsNullOrWhiteSpace(c))
.Select(c => c.Trim())
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs
index 69fd9ce..34e7f35 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using FoodLabeling.Application.Services.DbModels;
using SqlSugar;
@@ -128,6 +129,88 @@ public static class ReportsPrintLogDailyLabelIdHelper
return result;
}
+ ///
+ /// App Print Log 单日筛选:解析 / 为服务器本地自然日。
+ /// 两者均未传时返回 null(不按日过滤,兼容 App 未传 printDate 的历史行为)。
+ ///
+ public static DateTime? ResolvePrintLogFilterCalendarDay(DateTime? printDate, string? printDateDay)
+ {
+ var text = printDateDay?.Trim();
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ if (DateTime.TryParseExact(
+ text,
+ "yyyy-MM-dd",
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out var parsedExact))
+ {
+ return parsedExact.Date;
+ }
+
+ if (DateTime.TryParse(
+ text,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.AllowWhiteSpaces,
+ out var parsed))
+ {
+ return parsed.Date;
+ }
+
+ throw new Volo.Abp.UserFriendlyException("printDate 格式不正确,请使用 yyyy-MM-dd");
+ }
+
+ if (!printDate.HasValue)
+ {
+ return null;
+ }
+
+ var value = printDate.Value;
+ return value.Kind == DateTimeKind.Utc ? value.ToLocalTime().Date : value.Date;
+ }
+
+ ///
+ /// 按自然日过滤打印任务(MySQL DATE(IFNULL(PrintedAt, CreationTime)),避免 DateTime 区间比较时区偏差)。
+ ///
+ public static ISugarQueryable ApplyPrintTaskCalendarDayFilter(
+ ISugarQueryable query,
+ DateTime filterDay)
+ {
+ var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ return query.Where("DATE(IFNULL(PrintedAt, CreationTime)) = @filterDay", new { filterDay = dayText });
+ }
+
+ ///
+ /// 多表 Join 查询按自然日过滤(首表别名 t)。
+ ///
+ public static ISugarQueryable ApplyPrintTaskCalendarDayFilter(
+ ISugarQueryable query,
+ DateTime filterDay)
+ {
+ var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ return query.Where("DATE(IFNULL(t.PrintedAt, t.CreationTime)) = @filterDay", new { filterDay = dayText });
+ }
+
+ ///
+ /// 五表 Join(首表别名 t)。
+ ///
+ public static ISugarQueryable ApplyPrintTaskCalendarDayFilter(
+ ISugarQueryable query,
+ DateTime filterDay)
+ {
+ var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ return query.Where("DATE(IFNULL(t.PrintedAt, t.CreationTime)) = @filterDay", new { filterDay = dayText });
+ }
+
+ ///
+ /// App Print Log 单日筛选:printDate 的自然日区间;未传则默认服务器当天。
+ ///
+ public static (DateTime dayStart, DateTime dayEndExcl) ResolvePrintLogFilterDayRange(DateTime? printDate)
+ {
+ var day = ResolvePrintLogFilterCalendarDay(printDate, null) ?? DateTime.Today;
+ return (day, day.AddDays(1));
+ }
+
public readonly struct PrintTaskScopeKey
{
public PrintTaskScopeKey(string taskId, string? locationId, DateTime printedAt)
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs
index d752596..b49cfca 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs
@@ -36,7 +36,7 @@ public static class ReportsPrintLogExcelHelper
ws.Cell(r, 3).Value = x.ProductCategoryName ?? string.Empty;
ws.Cell(r, 4).Value = x.LabelCategoryName ?? string.Empty;
ws.Cell(r, 5).Value = x.TemplateText ?? string.Empty;
- ws.Cell(r, 6).Value = x.PrintedAt ?? string.Empty;
+ ws.Cell(r, 6).Value = ReportsDateTimeDisplayHelper.FormatPrintedAt(x.PrintedAt);
ws.Cell(r, 7).Value = x.PrintedByName ?? string.Empty;
ws.Cell(r, 8).Value = x.LocationText ?? string.Empty;
ws.Cell(r, 9).Value = x.ExpiryDateText ?? string.Empty;
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
index 5d0ed7f..e9e1a92 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
@@ -286,4 +286,8 @@ public static class ReportsPrintLogExpiryHelper
return null;
}
+
+ /// 解析并格式化为 Print Log Expiration 列展示。
+ public static string ExtractFormattedExpiryText(string? printInputJson) =>
+ ReportsDateTimeDisplayHelper.FormatExpiryDisplay(ExtractExpiryText(printInputJson));
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs
index e2911cf..5c46776 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs
@@ -9,6 +9,54 @@ namespace FoodLabeling.Application.Helpers;
///
public static class TeamMemberBatchExcelHelper
{
+ /// 导入/下载模板表头(与 PDF 导出 Region 列对齐)
+ public static readonly string[] ImportTemplateHeaders =
+ {
+ "Name",
+ "User Name",
+ "Password",
+ "Email",
+ "Phone",
+ "Role Id",
+ "Role Name",
+ "Region",
+ "Assigned Location Ids",
+ "Status"
+ };
+
+ /// 生成批量导入模板 xlsx(含 Region 可选列说明行)
+ public static MemoryStream BuildImportTemplateWorkbook()
+ {
+ var ms = new MemoryStream();
+ using var wb = new XLWorkbook();
+ var ws = wb.AddWorksheet("TeamMembers");
+ for (var i = 0; i < ImportTemplateHeaders.Length; i++)
+ {
+ ws.Cell(1, i + 1).Value = ImportTemplateHeaders[i];
+ ws.Cell(1, i + 1).Style.Font.Bold = true;
+ }
+
+ ws.Cell(2, 1).Value = "John Doe";
+ ws.Cell(2, 2).Value = "john.doe";
+ ws.Cell(2, 3).Value = "ChangeMe123!";
+ ws.Cell(2, 4).Value = "john@example.com";
+ ws.Cell(2, 5).Value = "789654444";
+ ws.Cell(2, 6).Value = "";
+ ws.Cell(2, 7).Value = "Staff";
+ ws.Cell(2, 8).Value = "";
+ ws.Cell(2, 9).Value = "LOC001;LOC002";
+ ws.Cell(2, 10).Value = "TRUE";
+
+ ws.Cell(3, 7).Value = "(Role Name 与系统角色名一致;Company Admin 可只填 Region 或留空由 Company 规则处理)";
+ ws.Cell(4, 8).Value = "(可选)Region 名称或 fl_group.Id,多个用 ; 分隔";
+ ws.Cell(5, 9).Value = "(可选)门店 LocationCode 或 location.Id(Guid),多个用 ; 分隔;与 Region 至少填一项";
+
+ ws.Columns().AdjustToContents();
+ wb.SaveAs(ms);
+ ms.Position = 0;
+ return ms;
+ }
+
///
/// 从上传的 Excel 解析为创建入参列表(行号从 2 起为数据行)。
///
@@ -162,7 +210,10 @@ public static class TeamMemberBatchExcelHelper
"password" or "pwd" or "密码" => "password",
"phone" or "mobile" or "电话" or "手机" => "phone",
"role" or "rolename" or "角色" => "rolename",
- "assignedlocations" or "locations" or "location" or "分配门店" or "门店" => "locations",
+ "roleid" or "角色id" => "roleid",
+ "region" or "regions" or "group" or "groupname" or "groupid" or "区域" => "regions",
+ "assignedlocations" or "assignedlocationids" or "locationids" or "locationcodes" or
+ "locations" or "location" or "分配门店" or "门店" or "门店id" => "locations",
"status" or "active" or "state" or "启用" => "status",
_ => null
};
@@ -221,7 +272,9 @@ public static class TeamMemberBatchExcelHelper
var userName = GetCellByField(colMap, ws, rowNum, "username");
var password = GetCellByField(colMap, ws, rowNum, "password");
var phoneStr = GetCellByField(colMap, ws, rowNum, "phone");
+ var roleIdCell = GetCellByField(colMap, ws, rowNum, "roleid");
var roleName = GetCellByField(colMap, ws, rowNum, "rolename");
+ var regionsCell = GetCellByField(colMap, ws, rowNum, "regions");
var locationsCell = GetCellByField(colMap, ws, rowNum, "locations");
var statusStr = GetCellByField(colMap, ws, rowNum, "status");
@@ -264,9 +317,13 @@ public static class TeamMemberBatchExcelHelper
}
Guid? roleIdResolved = null;
- if (string.IsNullOrWhiteSpace(roleName))
+ if (Guid.TryParse(roleIdCell?.Trim(), out var roleGuid))
{
- errors.Add("Role 不能为空");
+ roleIdResolved = roleGuid;
+ }
+ else if (string.IsNullOrWhiteSpace(roleName))
+ {
+ errors.Add("Role Name 不能为空(或填写有效的 Role Id Guid)");
}
else if (!roleNameToId.TryGetValue(NormalizeRoleKey(roleName), out var rid))
{
@@ -277,10 +334,13 @@ public static class TeamMemberBatchExcelHelper
roleIdResolved = rid;
}
- var locationTokens = SplitLocationTokens(locationsCell);
- if (locationTokens.Count == 0)
+ var regionTokens = SplitMultiValueTokens(regionsCell);
+ var locationTokens = SplitMultiValueTokens(locationsCell);
+ var isCompanyAdmin = TeamMemberRoleHelper.IsCompanyAdminRoleName(roleName);
+
+ if (regionTokens.Count == 0 && locationTokens.Count == 0 && !isCompanyAdmin)
{
- errors.Add("Assigned Locations 不能为空(多个门店可用分号、竖线或换行分隔)");
+ errors.Add("Region 与 Assigned Location Ids 至少填一项(均可留空时仅适用于 Company Admin 且已在 Web 端配置 Company)");
}
if (errors.Count > 0)
@@ -297,6 +357,7 @@ public static class TeamMemberBatchExcelHelper
Password = pwd,
Phone = phone,
RoleId = roleIdResolved,
+ RegionIds = regionTokens,
LocationIds = locationTokens,
State = state
};
@@ -308,23 +369,27 @@ public static class TeamMemberBatchExcelHelper
}
///
- /// 拆分门店单元格为「待解析」片段(后续由服务层解析为 Location Id)。
+ /// 拆分单元格为多个 token(门店/区域等;后续由服务层解析为 Id)。
///
- public static List SplitLocationTokens(string locationsCell)
+ public static List SplitMultiValueTokens(string cell)
{
- if (string.IsNullOrWhiteSpace(locationsCell))
+ if (string.IsNullOrWhiteSpace(cell))
{
return new List();
}
- var parts = locationsCell
+ return cell
.Split(new[] { ';', '|', '\n', '\r', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => !string.IsNullOrEmpty(x))
.ToList();
- return parts;
}
+ ///
+ /// 兼容旧调用。
+ ///
+ public static List SplitLocationTokens(string locationsCell) => SplitMultiValueTokens(locationsCell);
+
private static string RegexDigitsOnly(string s)
{
return new string(s.Where(char.IsDigit).ToArray());
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs
index 08eb869..057a0be 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs
@@ -9,6 +9,44 @@ namespace FoodLabeling.Application.Helpers;
public static class TeamMemberListScopeHelper
{
///
+ /// 统一 Guid 字符串键(小写 D 格式),避免 userlocation.UserId 与 User.Id.ToString() 大小写不一致导致漏人。
+ ///
+ public static string NormalizeScopeKey(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return string.Empty;
+ }
+
+ return Guid.TryParse(value.Trim(), out var guid)
+ ? guid.ToString("D").ToLowerInvariant()
+ : value.Trim().ToLowerInvariant();
+ }
+
+ public static string UserKey(Guid userId) => userId.ToString("D").ToLowerInvariant();
+
+ public static HashSet ParseGuidHashSet(IEnumerable raw)
+ {
+ var set = new HashSet();
+ foreach (var item in raw)
+ {
+ if (string.IsNullOrWhiteSpace(item))
+ {
+ continue;
+ }
+
+ if (Guid.TryParse(item.Trim(), out var guid))
+ {
+ set.Add(guid);
+ }
+ }
+
+ return set;
+ }
+
+ public static List ParseGuidList(IEnumerable raw) =>
+ ParseGuidHashSet(raw).ToList();
+ ///
/// 解析列表/导出适用的门店 Id 范围(用于 userlocation 命中成员)。
///
/// - 管理员:仅受 Query 中 partnerId / groupId / locationId 约束;均未传则返回 null(不限制成员)。
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs
new file mode 100644
index 0000000..e5663c1
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs
@@ -0,0 +1,95 @@
+using SqlSugar;
+using Volo.Abp.Users;
+using Yi.Framework.Rbac.Domain.Entities;
+
+namespace FoodLabeling.Application.Helpers;
+
+///
+/// Team Member 角色识别(Company Admin / Partner Admin 等)。
+///
+public static class TeamMemberRoleHelper
+{
+ ///
+ /// UI 展示为 Company Admin,库内常见为 Partner Admin 或 RoleCode 含 partner。
+ ///
+ public static bool IsCompanyAdminRoleName(string? roleName)
+ {
+ if (string.IsNullOrWhiteSpace(roleName))
+ {
+ return false;
+ }
+
+ var name = roleName.Trim();
+ if (string.Equals(name, "Company Admin", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ if (string.Equals(name, "Partner Admin", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return name.Contains("partner", StringComparison.OrdinalIgnoreCase) &&
+ name.Contains("admin", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// 当前登录用户是否为 Company Admin 类角色。
+ ///
+ public static async Task IsCompanyAdminUserAsync(ISqlSugarClient db, ICurrentUser currentUser)
+ {
+ if (currentUser.Id is null)
+ {
+ return false;
+ }
+
+ var roleId = await db.Queryable()
+ .Where(ur => ur.UserId == currentUser.Id.Value)
+ .Select(ur => (Guid?)ur.RoleId)
+ .FirstAsync();
+
+ return await IsCompanyAdminRoleAsync(db, roleId);
+ }
+
+ ///
+ /// 是否 Company Admin 类角色(按 RoleId 查库)。
+ ///
+ public static async Task IsCompanyAdminRoleAsync(ISqlSugarClient db, Guid? roleId)
+ {
+ if (roleId is null)
+ {
+ return false;
+ }
+
+ var role = await db.Queryable()
+ .FirstAsync(x => !x.IsDeleted && x.Id == roleId.Value);
+ if (role is null)
+ {
+ return false;
+ }
+
+ if (IsCompanyAdminRoleName(role.RoleName))
+ {
+ return true;
+ }
+
+ var code = role.RoleCode?.Trim();
+ return !string.IsNullOrEmpty(code) &&
+ code.Contains("partner", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。
+ ///
+ public static string? FormatDisplayRoleName(string? roleName)
+ {
+ if (IsCompanyAdminRoleName(roleName) &&
+ !string.Equals(roleName?.Trim(), "Company Admin", StringComparison.OrdinalIgnoreCase))
+ {
+ return "Company Admin";
+ }
+
+ return roleName;
+ }
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs
index 5222935..10f9c4e 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using FoodLabeling.Application.Contracts;
+using FoodLabeling.Application.Contracts.Dtos.AuthScope;
using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
@@ -94,11 +95,13 @@ public static class UsAppAuthScopeHelper
var partner = await db.Queryable()
.FirstAsync(x => !x.IsDeleted && x.Id == partnerId.Trim());
- var locations = await db.Queryable()
+ var locations = (await db.Queryable()
.Where(x => !x.IsDeleted && ids.Contains(x.Id.ToString()))
.OrderBy(x => x.OrderNum)
- .ThenBy(x => x.LocationName)
- .ToListAsync();
+ .ToListAsync())
+ .OrderBy(x => x.OrderNum)
+ .ThenBy(x => x.LocationName, StringComparer.OrdinalIgnoreCase)
+ .ToList();
return locations.Select(x => new AuthScopeLocationOptionDto
{
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs
index 674b99b..b05d2e9 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs
@@ -54,9 +54,11 @@ public class DashboardAppService : ApplicationService, IDashboardAppService
db.Queryable(), scopeLocationIds)
.CountAsync(x => x.CreationTime >= yesterdayStart && x.CreationTime < todayStart);
- var activeTemplates = await _dbContext.SqlSugarClient.Queryable()
+ var activeTemplates = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.CountAsync(x => !x.IsDeleted && x.State);
- var activeTemplatesPrevWeek = await _dbContext.SqlSugarClient.Queryable()
+ var activeTemplatesPrevWeek = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart);
var activeUsers = await DashboardScopeHelper.CountScopedTeamMembersAsync(
@@ -166,7 +168,7 @@ public class DashboardAppService : ApplicationService, IDashboardAppService
.Select((t, l, p, tpl) => new
{
t.Id,
- LabelCode = l.LabelCode,
+ t.LocationId,
LabelName = l.LabelName,
ProductName = p.ProductName,
tpl.Width,
@@ -178,6 +180,13 @@ public class DashboardAppService : ApplicationService, IDashboardAppService
})
.ToListAsync();
+ var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync(
+ db,
+ recentRaw.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey(
+ x.Id,
+ x.LocationId,
+ x.PrintedAt ?? DateTime.MinValue)).ToList());
+
var recentUserIds = recentRaw
.Select(x => x.CreatedBy)
.Where(x => !string.IsNullOrWhiteSpace(x))
@@ -209,10 +218,11 @@ public class DashboardAppService : ApplicationService, IDashboardAppService
: (string.IsNullOrWhiteSpace(x.LabelName) ? "无" : x.LabelName.Trim());
var printedAt = x.PrintedAt ?? DateTime.MinValue;
var status = ResolveRecentLabelStatus(x.PrintInputJson);
+ var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无";
return new DashboardRecentLabelItemDto
{
TaskId = x.Id,
- LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(),
+ LabelCode = labelDisplayId,
DisplayName = displayName,
PrintedByUserId = x.CreatedBy?.Trim(),
PrintedByName = ResolveRecentUserName(recentUserMap, x.CreatedBy),
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs
index 5ff50b8..06031ad 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs
@@ -34,12 +34,6 @@ public class FlLabelTemplateDbEntity
public string AppliedLocationType { get; set; } = "ALL";
- /// 适用 Company 范围:ALL / SPECIFIED(见 fl_label_template_partner)
- public string AppliedPartnerType { get; set; } = "ALL";
-
- /// 适用 Region 范围:ALL / SPECIFIED(见 fl_label_template_region)
- public string AppliedRegionType { get; set; } = "ALL";
-
public bool ShowRuler { get; set; }
public bool ShowGrid { get; set; }
@@ -54,4 +48,3 @@ public class FlLabelTemplateDbEntity
public bool State { get; set; }
}
-
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 9810f42..eddfd88 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
@@ -298,7 +298,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
throw new UserFriendlyException("标签不存在");
}
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => x.Id == label.TemplateId);
var category = await _dbContext.SqlSugarClient.Queryable()
.FirstAsync(x => x.Id == label.LabelCategoryId);
@@ -404,7 +405,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
input.LocationId,
input.LocationIds);
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim());
if (template is null)
{
@@ -520,7 +522,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
input.LocationId,
input.LocationIds);
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim());
if (template is null)
{
@@ -671,7 +674,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
}
// 取模板头 & elements
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => !x.IsDeleted && x.Id == label.TemplateId);
if (template is null)
{
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs
index 5b0df03..02534da 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs
@@ -29,10 +29,11 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
public async Task> GetListAsync(LabelTemplateGetListInputVo input)
{
+ input ??= new LabelTemplateGetListInputVo();
RefAsync total = 0;
var keyword = input.Keyword?.Trim();
- var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
- _dbContext.SqlSugarClient, input.GroupId, input.LocationId);
+ var scopedLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync(
+ _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId);
var query = _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted)
@@ -44,25 +45,30 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
query = await LabelTemplateScopeHelper.ApplyTemplateScopeFilterAsync(
_dbContext.SqlSugarClient, query, scopedLocationIds);
- query = !string.IsNullOrWhiteSpace(input.Sorting)
- ? query.OrderBy(input.Sorting)
- : query.OrderByDescending(x => x.LastModificationTime ?? x.CreationTime);
+ query = ApplyLabelTemplateListSorting(query, input.Sorting);
+ query = LabelTemplateQueryHelper.ProjectListColumns(query);
- var pageEntities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
+ var pageSize = input.MaxResultCount <= 0 ? 10 : input.MaxResultCount;
+ var pageEntities = await query.ToPageListAsync(input.SkipCount, pageSize, total);
var templateIds = pageEntities.Select(x => x.Id).ToList();
- // element count (Contents)
- var elementCounts = await _dbContext.SqlSugarClient.Queryable()
- .Where(x => templateIds.Contains(x.TemplateId))
- .GroupBy(x => x.TemplateId)
- .Select(x => new { TemplateId = x.TemplateId, Count = SqlFunc.AggregateCount(x.Id) })
- .ToListAsync();
- var elementCountMap = elementCounts.ToDictionary(x => x.TemplateId, x => (int)x.Count);
+ var elementCountMap = new Dictionary(StringComparer.Ordinal);
+ if (templateIds.Count > 0)
+ {
+ var elementCounts = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => templateIds.Contains(x.TemplateId))
+ .GroupBy(x => x.TemplateId)
+ .Select(x => new { TemplateId = x.TemplateId, Count = SqlFunc.AggregateCount(x.Id) })
+ .ToListAsync();
+ elementCountMap = elementCounts.ToDictionary(x => x.TemplateId, x => (int)x.Count);
+ }
var scopeMap = await LabelTemplateScopeHelper.BuildScopeDisplayMapAsync(
_dbContext.SqlSugarClient, pageEntities);
- var itemsMap = await LabelTemplateListItemsHelper.ResolveTemplateItemsMapAsync(
- _dbContext.SqlSugarClient, templateIds);
+ var itemsMap = templateIds.Count > 0
+ ? await LabelTemplateListItemsHelper.ResolveTemplateItemsMapAsync(
+ _dbContext.SqlSugarClient, templateIds)
+ : new Dictionary(StringComparer.Ordinal);
var items = pageEntities.Select(x =>
{
@@ -97,12 +103,69 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
};
}).ToList();
- return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
+ return BuildPagedResult(input.SkipCount, pageSize, total, items);
+ }
+
+ ///
+ /// 列表排序:白名单字段 + IFNULL(LastModificationTime, CreationTime),避免 ?? 与原始 Sorting 拼接导致 SQL 异常。
+ ///
+ private static ISugarQueryable ApplyLabelTemplateListSorting(
+ ISugarQueryable query,
+ string? sorting)
+ {
+ if (!string.IsNullOrWhiteSpace(sorting))
+ {
+ var s = sorting.Trim();
+ if (s.Equals("TemplateName asc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderBy(x => x.TemplateName);
+ }
+
+ if (s.Equals("TemplateName desc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderByDescending(x => x.TemplateName);
+ }
+
+ if (s.Equals("TemplateCode asc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderBy(x => x.TemplateCode);
+ }
+
+ if (s.Equals("TemplateCode desc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderByDescending(x => x.TemplateCode);
+ }
+
+ if (s.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderBy(x => x.CreationTime);
+ }
+
+ if (s.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderByDescending(x => x.CreationTime);
+ }
+
+ if (s.Equals("LastModificationTime asc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderBy(x => x.LastModificationTime);
+ }
+
+ if (s.Equals("LastModificationTime desc", StringComparison.OrdinalIgnoreCase))
+ {
+ return query.OrderByDescending(x => x.LastModificationTime);
+ }
+ }
+
+ return query
+ .OrderBy(x => SqlFunc.IsNull(x.LastModificationTime, x.CreationTime), OrderByType.Desc)
+ .OrderBy(x => x.TemplateCode, OrderByType.Asc);
}
public async Task GetAsync(string id)
{
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => !x.IsDeleted && x.TemplateCode == id);
if (template is null)
{
@@ -180,8 +243,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
Width = template.Width,
Height = template.Height,
AppliedLocationType = template.AppliedLocationType,
- AppliedPartnerType = template.AppliedPartnerType,
- AppliedRegionType = template.AppliedRegionType,
+ AppliedPartnerType = LabelTemplateScopeHelper.ScopeAll,
+ AppliedRegionType = LabelTemplateScopeHelper.ScopeAll,
ShowRuler = template.ShowRuler,
ShowGrid = template.ShowGrid,
BorderType = NormalizeTemplateBorderType(template.BorderType),
@@ -210,7 +273,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
throw new UserFriendlyException("模板名称不能为空");
}
- var duplicated = await _dbContext.SqlSugarClient.Queryable()
+ var duplicated = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.AnyAsync(x => !x.IsDeleted && x.TemplateCode == code);
if (duplicated)
{
@@ -237,8 +301,6 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
Width = input.Width,
Height = input.Height,
AppliedLocationType = scope.AppliedLocationType,
- AppliedPartnerType = scope.AppliedPartnerType,
- AppliedRegionType = scope.AppliedRegionType,
ShowRuler = input.ShowRuler,
ShowGrid = input.ShowGrid,
BorderType = NormalizeTemplateBorderType(input.BorderType),
@@ -262,13 +324,20 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
CurrentUser?.Id?.ToString(),
now);
+ await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync(
+ _dbContext.SqlSugarClient,
+ entity.Id,
+ scope.AppliedPartnerType,
+ scope.AppliedRegionType);
+
return await GetAsync(code);
}
[UnitOfWork]
public async Task UpdateAsync(string id, LabelTemplateUpdateInputVo input)
{
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => !x.IsDeleted && x.TemplateCode == id);
if (template is null)
{
@@ -279,7 +348,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
var name = input.TemplateName?.Trim();
if (!string.IsNullOrWhiteSpace(code) && !string.Equals(code, template.TemplateCode, StringComparison.OrdinalIgnoreCase))
{
- var duplicated = await _dbContext.SqlSugarClient.Queryable()
+ var duplicated = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.AnyAsync(x => !x.IsDeleted && x.TemplateCode == code);
if (duplicated)
{
@@ -295,8 +365,6 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
template.Width = input.Width;
template.Height = input.Height;
template.AppliedLocationType = scope.AppliedLocationType;
- template.AppliedPartnerType = scope.AppliedPartnerType;
- template.AppliedRegionType = scope.AppliedRegionType;
template.ShowRuler = input.ShowRuler;
template.ShowGrid = input.ShowGrid;
template.BorderType = NormalizeTemplateBorderType(input.BorderType);
@@ -325,13 +393,20 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
CurrentUser?.Id?.ToString(),
template.LastModificationTime ?? DateTime.Now);
+ await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync(
+ _dbContext.SqlSugarClient,
+ template.Id,
+ scope.AppliedPartnerType,
+ scope.AppliedRegionType);
+
return await GetAsync(template.TemplateCode);
}
[UnitOfWork]
public async Task DeleteAsync(string id)
{
- var template = await _dbContext.SqlSugarClient.Queryable()
+ var template = await LabelTemplateQueryHelper.ProjectListColumns(
+ _dbContext.SqlSugarClient.Queryable())
.FirstAsync(x => !x.IsDeleted && x.TemplateCode == id);
if (template is null)
{
@@ -358,12 +433,15 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
await _dbContext.SqlSugarClient.Deleteable()
.Where(x => x.TemplateId == template.Id)
.ExecuteCommandAsync();
- await _dbContext.SqlSugarClient.Deleteable()
- .Where(x => x.TemplateId == template.Id)
- .ExecuteCommandAsync();
- await _dbContext.SqlSugarClient.Deleteable()
- .Where(x => x.TemplateId == template.Id)
- .ExecuteCommandAsync();
+ if (await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(_dbContext.SqlSugarClient))
+ {
+ await _dbContext.SqlSugarClient.Deleteable()
+ .Where(x => x.TemplateId == template.Id)
+ .ExecuteCommandAsync();
+ await _dbContext.SqlSugarClient.Deleteable()
+ .Where(x => x.TemplateId == template.Id)
+ .ExecuteCommandAsync();
+ }
await _dbContext.SqlSugarClient.Deleteable()
.Where(x => x.TemplateId == template.Id)
.ExecuteCommandAsync();
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
index cb373b3..b3ce8da 100644
--- 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
@@ -135,7 +135,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
: x.LabelCategoryName!.Trim(),
CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat,
TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText,
- PrintedAt = ReportsDateTimeDisplayHelper.FormatPrintedAt(printedAt),
+ PrintedAt = printedAt,
PrintedByName = ResolveUserName(userMap, x.CreatedBy),
LocationText = locText,
LocationId = x.LocationId?.Trim(),
@@ -997,7 +997,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
: x.LabelCategoryName!.Trim(),
CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat,
TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText,
- PrintedAt = ReportsDateTimeDisplayHelper.FormatPrintedAt(printedAt),
+ PrintedAt = printedAt,
PrintedByName = ResolveUserName(userMap, x.CreatedBy),
LocationText = locText,
LocationId = x.LocationId?.Trim(),
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs
index 44dc45c..905da64 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs
@@ -120,6 +120,14 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
_dbContext.SqlSugarClient, locationIds);
+ if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, role?.RoleId) &&
+ partnerIds.Count > 0)
+ {
+ (regionIds, assigned) = await ApplyCompanyAdminDisplayScopeAsync(
+ role?.RoleId, partnerIds, regionIds, assigned);
+ locationIds = assigned.Select(x => x.Id).ToList();
+ }
+
return new TeamMemberGetOutputDto
{
Id = user.Id,
@@ -140,7 +148,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
///
public async Task CreateAsync(TeamMemberCreateInputVo input)
{
- var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input);
+ var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input, input.RoleId);
var user = new UserAggregateRoot
{
@@ -245,25 +253,13 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
public Task DownloadTeamMemberImportTemplateAsync()
{
var opt = _batchImportOptions.Value;
- var dir = opt.TemplateDirectory?.Trim();
- if (string.IsNullOrWhiteSpace(dir))
- {
- throw new UserFriendlyException("未配置批量导入模板目录 FoodLabeling:BatchImport:TemplateDirectory");
- }
-
var fileName = opt.TeamMemberTemplateFileName?.Trim();
if (string.IsNullOrWhiteSpace(fileName))
{
- throw new UserFriendlyException("未配置模板文件名 FoodLabeling:BatchImport:TeamMemberTemplateFileName");
+ fileName = "Team-Member-批量导入模板.xlsx";
}
- var fullPath = Path.Combine(dir, fileName);
- if (!File.Exists(fullPath))
- {
- throw new UserFriendlyException($"模板文件不存在:{fullPath}");
- }
-
- var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ var stream = TeamMemberBatchExcelHelper.BuildImportTemplateWorkbook();
const string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
return Task.FromResult(new FileStreamResult(stream, contentType)
{
@@ -294,6 +290,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
scopeLocationIds,
restrictAssignedLocationsToFilter: scopeLocationIds is not null);
+ var regionNameMap = await LoadRegionNameMapAsync(
+ rows.SelectMany(r => r.RegionIds).Distinct(StringComparer.Ordinal));
+
var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
var document = Document.Create(container =>
{
@@ -306,11 +305,12 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
{
table.ColumnsDefinition(c =>
{
+ c.RelativeColumn(1.3f);
+ c.RelativeColumn(1.5f);
+ c.RelativeColumn(1.0f);
+ c.RelativeColumn(1.0f);
c.RelativeColumn(1.4f);
- c.RelativeColumn(1.6f);
- c.RelativeColumn(1.1f);
- c.RelativeColumn(1.1f);
- c.RelativeColumn(2.2f);
+ c.RelativeColumn(2.0f);
c.RelativeColumn(0.7f);
});
@@ -321,11 +321,19 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
table.Cell().Element(CellHeader).Text("Email");
table.Cell().Element(CellHeader).Text("Phone");
table.Cell().Element(CellHeader).Text("Role");
+ table.Cell().Element(CellHeader).Text("Region");
table.Cell().Element(CellHeader).Text("Assigned Locations");
table.Cell().Element(CellHeader).Text("Status");
foreach (var e in rows)
{
+ var regionText = e.RegionIds.Count == 0
+ ? "无"
+ : string.Join("; ",
+ e.RegionIds.Select(id =>
+ regionNameMap.TryGetValue(id, out var name) && !string.IsNullOrWhiteSpace(name)
+ ? name
+ : id));
var locText = e.AssignedLocations.Count == 0
? "无"
: string.Join("; ",
@@ -341,6 +349,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(string.IsNullOrWhiteSpace(e.RoleName) ? "无" : e.RoleName);
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
+ .Text(regionText);
+ table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(locText);
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(status);
@@ -399,7 +409,16 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
{
try
{
- vo.LocationIds = await ResolveLocationIdsFromImportTokensAsync(vo.LocationIds);
+ if (vo.RegionIds is { Count: > 0 })
+ {
+ vo.RegionIds = await ResolveRegionIdsFromImportTokensAsync(vo.RegionIds);
+ }
+
+ if (vo.LocationIds is { Count: > 0 })
+ {
+ vo.LocationIds = await ResolveLocationIdsFromImportTokensAsync(vo.LocationIds);
+ }
+
await CreateAsync(vo);
result.SuccessCount++;
}
@@ -515,7 +534,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
.FirstAsync();
if (byCode is null)
{
- throw new UserFriendlyException($"未找到门店编码:{key}");
+ throw new UserFriendlyException($"未找到门店 LocationCode:{key}(亦可填 location.Id Guid)");
}
result.Add(byCode.Id.ToString());
@@ -524,6 +543,83 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
return result.Distinct().ToList();
}
+ private async Task> ResolveRegionIdsFromImportTokensAsync(List tokens)
+ {
+ var result = new List();
+ foreach (var raw in tokens)
+ {
+ var s = raw.Trim();
+ if (string.IsNullOrEmpty(s))
+ {
+ continue;
+ }
+
+ var idx = s.IndexOf(" -", StringComparison.Ordinal);
+ var key = idx > 0 ? s[..idx].Trim() : s.Trim();
+
+ if (Guid.TryParse(key, out _))
+ {
+ var byId = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted && x.Id == key)
+ .FirstAsync();
+ if (byId is null)
+ {
+ throw new UserFriendlyException($"无效 Region Id:{key}");
+ }
+
+ result.Add(byId.Id.Trim());
+ continue;
+ }
+
+ var matches = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted)
+ .ToListAsync();
+ matches = matches
+ .Where(x => string.Equals(x.GroupName?.Trim(), key, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (matches.Count == 0)
+ {
+ throw new UserFriendlyException($"未找到 Region:{key}(可填 fl_group.Id 或 GroupName)");
+ }
+
+ if (matches.Count > 1)
+ {
+ throw new UserFriendlyException(
+ $"Region 名称「{key}」存在多条记录,请改用 fl_group.Id(Guid)");
+ }
+
+ result.Add(matches[0].Id.Trim());
+ }
+
+ return result.Distinct(StringComparer.Ordinal).ToList();
+ }
+
+ private async Task> LoadRegionNameMapAsync(IEnumerable regionIds)
+ {
+ var ids = regionIds
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
+ if (ids.Count == 0)
+ {
+ return new Dictionary(StringComparer.Ordinal);
+ }
+
+ var groups = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted && ids.Contains(x.Id))
+ .Select(x => new { x.Id, x.GroupName })
+ .ToListAsync();
+
+ return groups
+ .Where(x => !string.IsNullOrWhiteSpace(x.Id))
+ .ToDictionary(
+ x => x.Id.Trim(),
+ x => x.GroupName?.Trim() ?? x.Id.Trim(),
+ StringComparer.Ordinal);
+ }
+
private async Task> BuildFilteredUserQueryAsync(
TeamMemberGetListInputVo input,
List? scopeLocationIds)
@@ -555,13 +651,30 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
}
else
{
- var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal);
- var userIdStrs = await _dbContext.SqlSugarClient.Queryable()
- .Where(x => !x.IsDeleted && scopeSet.Contains(x.LocationId))
- .Select(x => x.UserId)
+ var scopeGuidSet = TeamMemberListScopeHelper.ParseGuidHashSet(scopeLocationIds);
+ var userLocationLinks = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted)
+ .Select(x => new { x.UserId, x.LocationId })
.ToListAsync();
- var allowed = new HashSet(userIdStrs);
- query = query.Where(u => allowed.Contains(u.Id.ToString()));
+
+ var allowedUserGuids = userLocationLinks
+ .Where(x =>
+ Guid.TryParse(x.LocationId, out var locGuid) && scopeGuidSet.Contains(locGuid))
+ .Select(x => x.UserId)
+ .Select(TeamMemberListScopeHelper.NormalizeScopeKey)
+ .Where(x => !string.IsNullOrEmpty(x))
+ .Select(x => Guid.Parse(x))
+ .Distinct()
+ .ToList();
+
+ if (allowedUserGuids.Count == 0)
+ {
+ query = query.Where(_ => false);
+ }
+ else
+ {
+ query = query.Where(u => allowedUserGuids.Contains(u.Id));
+ }
}
}
@@ -579,43 +692,62 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
}
var userIds = users.Select(x => x.Id).ToList();
- var userIdStrings = userIds.Select(x => x.ToString()).ToList();
+ var userGuidKeys = userIds.Select(TeamMemberListScopeHelper.UserKey).ToHashSet(StringComparer.Ordinal);
var userRolePairs = await _dbContext.SqlSugarClient.Queryable((ur, r) => ur.RoleId == r.Id)
.Where(ur => userIds.Contains(ur.UserId))
.Select((ur, r) => new { ur.UserId, r.Id, r.RoleName })
.ToListAsync();
- var roleMap = userRolePairs
+ var roleIdByUser = userRolePairs
.GroupBy(x => x.UserId)
- .ToDictionary(g => g.Key, g => g.FirstOrDefault());
+ .ToDictionary(g => g.Key, g => (Guid?)g.First().Id);
- var userLocQuery = _dbContext.SqlSugarClient.Queryable()
+ var allUserLocations = await _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted)
- .Where(x => userIdStrings.Contains(x.UserId));
- if (restrictAssignedLocationsToFilter && scopeLocationIds is { Count: > 0 })
- {
- var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal);
- userLocQuery = userLocQuery.Where(x => scopeSet.Contains(x.LocationId));
- }
+ .Select(x => new { x.UserId, x.LocationId })
+ .ToListAsync();
+
+ var scopeGuidSet = scopeLocationIds is { Count: > 0 }
+ ? TeamMemberListScopeHelper.ParseGuidHashSet(scopeLocationIds)
+ : null;
- var userLocations = await userLocQuery.ToListAsync();
+ var userLocations = allUserLocations
+ .Where(x => userGuidKeys.Contains(TeamMemberListScopeHelper.NormalizeScopeKey(x.UserId)))
+ .Where(x =>
+ scopeGuidSet is null ||
+ (Guid.TryParse(x.LocationId, out var locGuid) && scopeGuidSet.Contains(locGuid)))
+ .ToList();
var locationIds = userLocations.Select(x => x.LocationId).Distinct().ToList();
+ var locationGuidList = TeamMemberListScopeHelper.ParseGuidList(locationIds);
var locations = await _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted)
- .WhereIF(locationIds.Count > 0, x => locationIds.Contains(x.Id.ToString()))
+ .WhereIF(locationGuidList.Count > 0, x => locationGuidList.Contains(x.Id))
.Select(x => new { x.Id, x.LocationCode, x.LocationName })
.ToListAsync();
- var locationMap = locations.ToDictionary(x => x.Id.ToString(), x => x);
+ var locationMap = locations.ToDictionary(
+ x => TeamMemberListScopeHelper.UserKey(x.Id),
+ x => x);
var assignedMap = userLocations
- .GroupBy(x => x.UserId)
+ .GroupBy(x => TeamMemberListScopeHelper.NormalizeScopeKey(x.UserId))
.ToDictionary(
g => g.Key,
g => g.Select(x =>
{
- if (locationMap.TryGetValue(x.LocationId, out var loc))
+ var locKey = TeamMemberListScopeHelper.NormalizeScopeKey(x.LocationId);
+ if (locationMap.TryGetValue(locKey, out var loc))
+ {
+ return new TeamMemberAssignedLocationDto
+ {
+ Id = loc.Id.ToString(),
+ LocationCode = loc.LocationCode,
+ LocationName = loc.LocationName
+ };
+ }
+
+ if (locationMap.TryGetValue(x.LocationId, out loc))
{
return new TeamMemberAssignedLocationDto
{
@@ -628,15 +760,31 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
return null;
}).Where(x => x != null).Cast().ToList());
- var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap);
+ var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap, roleIdByUser);
- return users.Select(u =>
+ var items = new List();
+ foreach (var u in users)
{
- roleMap.TryGetValue(u.Id, out var role);
- assignedMap.TryGetValue(u.Id.ToString(), out var assigned);
- scopeIdsMap.TryGetValue(u.Id.ToString(), out var scopeIds);
+ roleIdByUser.TryGetValue(u.Id, out var listRoleId);
+ var roleName = userRolePairs.FirstOrDefault(x => x.UserId == u.Id)?.RoleName;
+ var userKey = TeamMemberListScopeHelper.UserKey(u.Id);
+ assignedMap.TryGetValue(userKey, out var assigned);
+ scopeIdsMap.TryGetValue(userKey, out var scopeIds);
+
+ var partnerIds = scopeIds?.PartnerIds ?? new List();
+ var regionIds = scopeIds?.RegionIds ?? new List();
+ var assignedLocations = assigned ?? new List();
+
+ (regionIds, assignedLocations) = await ApplyCompanyAdminDisplayScopeAsync(
+ listRoleId, partnerIds, regionIds, assignedLocations);
+
+ var locationIdList = assignedLocations
+ .Select(x => x.Id)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
- return new TeamMemberGetListOutputDto
+ items.Add(new TeamMemberGetListOutputDto
{
Id = u.Id,
FullName = u.Name ?? string.Empty,
@@ -644,17 +792,21 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
Email = u.Email,
Phone = u.Phone,
State = u.State,
- RoleId = role?.Id,
- RoleName = role?.RoleName,
- PartnerIds = scopeIds?.PartnerIds ?? new List(),
- RegionIds = scopeIds?.RegionIds ?? new List(),
- AssignedLocations = assigned ?? new List()
- };
- }).ToList();
+ RoleId = listRoleId,
+ RoleName = TeamMemberRoleHelper.FormatDisplayRoleName(roleName),
+ PartnerIds = partnerIds,
+ RegionIds = regionIds,
+ LocationIds = locationIdList,
+ AssignedLocations = assignedLocations
+ });
+ }
+
+ return items;
}
private async Task> BuildTeamMemberScopeIdsMapAsync(
- Dictionary> assignedMap)
+ Dictionary> assignedMap,
+ Dictionary roleIdByUser)
{
var result = new Dictionary(StringComparer.Ordinal);
foreach (var (userId, assigned) in assignedMap)
@@ -665,16 +817,29 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
.Select(x => x.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
- if (locationIds.Count == 0)
+
+ var partnerIds = locationIds.Count == 0
+ ? new List()
+ : await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(
+ _dbContext.SqlSugarClient, locationIds);
+ var regionIds = locationIds.Count == 0
+ ? new List()
+ : await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
+ _dbContext.SqlSugarClient, locationIds);
+
+ if (Guid.TryParse(userId, out var userGuid) &&
+ roleIdByUser.TryGetValue(userGuid, out var roleId) &&
+ await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) &&
+ partnerIds.Count > 0)
{
- result[userId] = new TeamMemberScopeIds();
- continue;
+ var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(
+ _dbContext.SqlSugarClient, partnerIds);
+ if (allRegions.Count > 0)
+ {
+ regionIds = allRegions;
+ }
}
- var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(
- _dbContext.SqlSugarClient, locationIds);
- var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
- _dbContext.SqlSugarClient, locationIds);
result[userId] = new TeamMemberScopeIds
{
PartnerIds = partnerIds,
@@ -685,6 +850,68 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
return result;
}
+ ///
+ /// Company Admin 列表/详情:展示所选 Company 下全部 Region 与门店。
+ ///
+ private async Task<(List RegionIds, List AssignedLocations)>
+ ApplyCompanyAdminDisplayScopeAsync(
+ Guid? roleId,
+ List partnerIds,
+ List regionIds,
+ List assigned)
+ {
+ if (!await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) ||
+ partnerIds.Count == 0)
+ {
+ return (regionIds, assigned);
+ }
+
+ var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(
+ _dbContext.SqlSugarClient, partnerIds);
+ var allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(
+ _dbContext.SqlSugarClient, partnerIds);
+ var allAssigned = await BuildAssignedLocationDtosAsync(allLocationIds);
+
+ return (
+ allRegions.Count > 0 ? allRegions : regionIds,
+ allAssigned.Count > 0 ? allAssigned : assigned);
+ }
+
+ private async Task> BuildAssignedLocationDtosAsync(
+ IReadOnlyList locationIds)
+ {
+ var ids = LocationScopeBindingHelper.NormalizeIds(locationIds);
+ if (ids.Count == 0)
+ {
+ return new List();
+ }
+
+ var guidList = ids
+ .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null)
+ .Where(x => x.HasValue)
+ .Select(x => x!.Value)
+ .ToList();
+ if (guidList.Count == 0)
+ {
+ return new List();
+ }
+
+ var locations = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted && guidList.Contains(x.Id))
+ .Select(x => new { x.Id, x.LocationCode, x.LocationName })
+ .ToListAsync();
+
+ return locations
+ .OrderBy(x => x.LocationCode)
+ .Select(x => new TeamMemberAssignedLocationDto
+ {
+ Id = x.Id.ToString(),
+ LocationCode = x.LocationCode,
+ LocationName = x.LocationName
+ })
+ .ToList();
+ }
+
private sealed class TeamMemberScopeIds
{
public List PartnerIds { get; init; } = new();
@@ -699,18 +926,41 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
RegionIds = input.RegionIds,
GroupIds = input.GroupIds,
LocationIds = input.LocationIds
- });
+ }, input.RoleId);
- private async Task> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberCreateInputVo input)
+ private async Task> ResolveTeamMemberLocationIdsForSaveAsync(
+ TeamMemberCreateInputVo input,
+ Guid? roleId)
{
var partnerIds = NormalizePartnerIds(input);
var regionIds = NormalizeRegionIds(input);
+ var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
+ var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(
+ _dbContext.SqlSugarClient, roleId);
+
+ if (isCompanyAdmin && partnerIds.Count > 0 &&
+ regionIds.Count == 0 && explicitLocationIds.Count == 0)
+ {
+ var fromPartner = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
+ _dbContext.SqlSugarClient, partnerIds, null, null);
+ if (fromPartner.Count == 0)
+ {
+ throw new UserFriendlyException("Company Admin 需绑定公司下门店,所选公司下暂无可用门店");
+ }
+
+ await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, fromPartner);
+ return fromPartner;
+ }
+
var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
_dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds);
if (merged.Count == 0)
{
- throw new UserFriendlyException("成员必须至少分配一个门店(公司/区域/门店至少选一项)");
+ throw new UserFriendlyException(
+ isCompanyAdmin
+ ? "Company Admin 必须选择 Company,或指定 Region / 门店"
+ : "成员必须至少分配一个门店(公司/区域/门店至少选一项)");
}
await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
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 5ef133b..704240c 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
@@ -7,6 +7,7 @@ using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using FoodLabeling.Application.Contracts;
+using FoodLabeling.Application.Contracts.Dtos.AuthScope;
using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Helpers;
@@ -209,7 +210,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
///
[Authorize]
public virtual async Task SelectAdminScopeLocationAsync(
- UsAppSelectAdminScopeLocationInputVo input)
+ AuthScopeSelectLocationInputVo input)
{
UsAppAuthScopeHelper.EnsureAdminAppToken(CurrentUser);
if (!CurrentUser.Id.HasValue)
@@ -221,7 +222,12 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
_dbContext.SqlSugarClient,
_distributedCache,
CurrentUser.Id.Value,
- input);
+ new UsAppSelectAdminScopeLocationInputVo
+ {
+ PartnerId = input.PartnerId,
+ GroupId = input.GroupId,
+ LocationId = input.LocationId
+ });
}
///
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 be42a9e..96a7f1e 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
@@ -396,12 +396,16 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
}
var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
+ var baseTime = input.BaseTime ?? DateTime.Now;
+ var dailyLabelId = await ReportsPrintLogDailyLabelIdHelper.ResolveNextDailyLabelIdAsync(
+ _dbContext.SqlSugarClient, locationId, baseTime);
var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
{
LabelCode = labelCode,
ProductId = previewProductId,
BaseTime = input.BaseTime,
+ LocationId = locationId,
PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value)
});
@@ -465,7 +469,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
return new UsAppLabelPreviewDto
{
- LabelId = labelRow.Id,
+ LabelId = dailyLabelId,
PrintLabelDisplayId = printLabelDisplayId,
LocationId = locationId,
LabelCode = labelCode,
@@ -568,6 +572,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
LabelCode = labelCode,
ProductId = previewProductId,
BaseTime = input.BaseTime,
+ LocationId = locationId,
PrintInputJson = normalizedPrintInput
});
@@ -856,6 +861,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
/// ```json
/// {
/// "locationId": "11111111-1111-1111-1111-111111111111",
+ /// "printDate": "2026-06-16T00:00:00",
/// "skipCount": 1,
/// "maxResultCount": 20
/// }
@@ -863,6 +869,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
///
/// 参数说明:
/// - locationId: 当前门店 Id(必填)
+ /// - printDate: 打印日期(自然日,可选;未传默认当天;按 PrintedAt ?? CreationTime 筛选)
/// - skipCount: 页码(从 1 开始,遵循本项目约定)
/// - maxResultCount: 每页条数
///
@@ -918,8 +925,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
RefAsync total = 0;
- var query = UsAppPrintLogScopeHelper.BuildLocationPrintTaskQuery(
- _dbContext.SqlSugarClient, locationId, restrictToCreator, currentUserIdStr)
+ var filterDay = ReportsPrintLogDailyLabelIdHelper.ResolvePrintLogFilterCalendarDay(
+ input.PrintDate, input.PrintDateDay);
+
+ var baseQuery = UsAppPrintLogScopeHelper.BuildLocationPrintTaskQuery(
+ _dbContext.SqlSugarClient, locationId, restrictToCreator, currentUserIdStr);
+
+ if (filterDay.HasValue)
+ {
+ baseQuery = ReportsPrintLogDailyLabelIdHelper.ApplyPrintTaskCalendarDayFilter(baseQuery, filterDay.Value);
+ }
+
+ var query = baseQuery
.OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc)
.OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc)
.Select((t, l, p, lt, tpl) => new
@@ -943,6 +960,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
+ var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync(
+ _dbContext.SqlSugarClient,
+ pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey(
+ x.Id,
+ locationId,
+ x.PrintedAt ?? x.CreationTime)).ToList());
+
var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync(
_dbContext.SqlSugarClient,
pageRows.Select(x => x.CreatedBy));
@@ -952,7 +976,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
TaskId = x.Id,
BatchId = x.BatchId,
CopyIndex = x.CopyIndex,
- LabelId = x.LabelId,
+ LabelId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无",
+ LabelEntityId = x.LabelId ?? string.Empty,
LabelCode = x.LabelCode ?? string.Empty,
ProductId = x.ProductId,
ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
index 60e46e4..90eb624 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
@@ -20,7 +20,7 @@
},
//应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049)
"App": {
- "SelfUrl": "http://localhost:19001",
+ "SelfUrl": "http://192.168.31.88:19001",
"CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:3000"
},
//配置
diff --git a/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx b/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx
index b0e3137..a4b96df 100755
--- a/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx
+++ b/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx
@@ -187,10 +187,19 @@ function normLocationId(id: string | null | undefined): string {
return String(id ?? "").trim().toLowerCase();
}
-function memberMatchesLocationScope(m: TeamMemberDto, allowedLocationIds: Set | null): boolean {
+function memberMatchesLocationScope(
+ m: TeamMemberDto,
+ allowedLocationIds: Set | null,
+ allowedPartnerId?: string | null,
+): boolean {
+ if (allowedPartnerId) {
+ const partnerKey = allowedPartnerId.trim().toLowerCase();
+ const partnerIds = (m.partnerIds ?? []).map((x) => String(x).trim().toLowerCase());
+ if (partnerIds.includes(partnerKey)) return true;
+ }
if (allowedLocationIds === null) return true;
if (allowedLocationIds.size === 0) return false;
- const ids = (m.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
+ const ids = (m.locationIds ?? []).map((x) => normLocationId(x)).filter(Boolean);
return ids.some((id) => allowedLocationIds.has(id));
}
@@ -575,7 +584,7 @@ export function PeopleView({
if (memberLocationFilter !== "all" || !memberUsesClientScopeFilter) return null;
const ids = new Set();
for (const loc of memberLocationsForSelect) {
- if (loc.id) ids.add(loc.id);
+ if (loc.id) ids.add(normLocationId(loc.id));
}
return ids;
}, [memberLocationFilter, memberUsesClientScopeFilter, memberLocationsForSelect]);
@@ -772,11 +781,14 @@ export function PeopleView({
skipCount: 1,
maxResultCount: 500,
keyword: debouncedMemberKeyword || undefined,
+ partnerId: memberPartnerFilter !== "all" ? memberPartnerFilter : undefined,
+ groupId: memberGroupFilter !== "all" ? memberGroupFilter : undefined,
},
ac.signal,
);
+ const allowedPartnerId = memberPartnerFilter !== "all" ? memberPartnerFilter : null;
const filtered = (res.items ?? []).filter((m) =>
- memberMatchesLocationScope(m, memberAllowedLocationIds),
+ memberMatchesLocationScope(m, memberAllowedLocationIds, allowedPartnerId),
);
const total = filtered.length;
const start = (memberPageIndex - 1) * memberPageSize;
@@ -788,6 +800,8 @@ export function PeopleView({
skipCount: Math.max(1, memberPageIndex),
maxResultCount: memberPageSize,
keyword: debouncedMemberKeyword || undefined,
+ partnerId: memberPartnerFilter !== "all" ? memberPartnerFilter : undefined,
+ groupId: memberGroupFilter !== "all" ? memberGroupFilter : undefined,
locationId: locationIdParam,
},
ac.signal,
diff --git a/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts b/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts
index 1f4929b..71ee3b9 100644
--- a/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts
+++ b/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts
@@ -168,6 +168,8 @@ export async function getTeamMembers(
MaxResultCount: input.maxResultCount,
Keyword: input.keyword,
RoleId: input.roleId,
+ PartnerId: input.partnerId,
+ GroupId: input.groupId,
LocationId: input.locationId,
State: input.state,
Sorting: input.sorting,
diff --git a/美国版/Food Labeling Management Platform/src/types/teamMember.ts b/美国版/Food Labeling Management Platform/src/types/teamMember.ts
index 7f6731a..bf1e715 100644
--- a/美国版/Food Labeling Management Platform/src/types/teamMember.ts
+++ b/美国版/Food Labeling Management Platform/src/types/teamMember.ts
@@ -35,6 +35,8 @@ export type TeamMemberGetListInput = {
maxResultCount: number; // pageSize
keyword?: string;
roleId?: string;
+ partnerId?: string;
+ groupId?: string;
locationId?: string;
state?: boolean;
sorting?: string;
diff --git a/项目相关文档/6-11代码优化.md b/项目相关文档/6-11代码优化.md
index d33d378..683873c 100644
--- a/项目相关文档/6-11代码优化.md
+++ b/项目相关文档/6-11代码优化.md
@@ -1,10 +1,17 @@
# 6-11 代码优化
-本文档说明 **2026-06-11** 对美国版 App **`POST /api/app/us-app-labeling/preview`** 出参 **`labelId`** 的格式调整。
+本文档说明 **2026-06-11** 对美国版 App 标签预览/打印相关的两项**纯后端**改造(**不改 Web / App 前端**):
+
+1. **`POST /api/app/us-app-labeling/preview`** 出参 **`labelId`** 改为门店当日序号 `yyyyMMdd-n`
+2. 模板 **Company 自动生成元素**:预览/打印时按门店从 **`fl_partner`** 填充公司名及可选地址字段
+
+测试环境:`http://flus-test.3ffoodsafety.com`
---
-## 背景
+## 一、Preview 出参 `labelId` 格式
+
+### 背景
预览页「Label ID」原先返回 **`fl_label.Id`**(GUID,如 `3a2192be-7b8f-e3e8-db9c-3a5e627b9222`),与业务要求不符。
@@ -18,11 +25,7 @@
格式:`{yyyyMMdd}-{n}`(`n` 从 1 递增,按 `PrintedAt ?? CreationTime` 所在自然日、同一 `locationId` 统计)。
-测试环境:`http://flus-test.3ffoodsafety.com`
-
----
-
-## 接口说明
+### 接口说明
| 项目 | 内容 |
|------|------|
@@ -30,7 +33,7 @@
| 路径 | `/api/app/us-app-labeling/preview` |
| 鉴权 | Bearer Token |
-### 入参(Body:`UsAppLabelPreviewInputVo`)
+#### 入参(Body:`UsAppLabelPreviewInputVo`)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
@@ -40,7 +43,7 @@
| `baseTime` | DateTime | 否 | 基准时间;影响模板内日期/时间控件,也用于确定「哪一天的序号」;未传则用服务端当前时间 |
| `printInputJson` | object | 否 | 打印时输入项 |
-### 出参(`UsAppLabelPreviewDto`)变更
+#### 出参(`UsAppLabelPreviewDto`)变更
| 字段 | 变更前 | 变更后 |
|------|--------|--------|
@@ -48,7 +51,7 @@
其余字段(`locationId`、`labelCode`、`template`、`labelLastEdited` 等)不变。
-### `labelId` 计算规则
+#### `labelId` 计算规则
1. 取 **`baseTime ?? 当前服务器时间`** 的日期部分 `yyyyMMdd`。
2. 统计该 **`locationId`** 在当日内已有打印任务数(`fl_label_print_task`,时间取 `PrintedAt ?? CreationTime`)。
@@ -57,9 +60,7 @@
> **注意**:`labelId` 不是 `fl_label.LabelCode`,也不是 `fl_label.Id`。标签主键如需内部关联,请使用打印任务创建后的 `taskId` 或 print-log 中的 `labelEntityId`。
----
-
-## 请求示例
+### 请求示例(labelId)
```bash
curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \
@@ -87,9 +88,7 @@ curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview"
若该门店在 `2026-05-13` 已有 2 条打印记录,预览返回 `20260513-3`;当日首条预览为 `20260513-1`。
----
-
-## 涉及代码
+### 涉及代码(labelId)
| 文件 | 说明 |
|------|------|
@@ -98,16 +97,14 @@ curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview"
| `Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs` | `labelId` XML 注释 |
| `IServices/IUsAppLabelingAppService.cs` | 接口注释 |
----
-
-## 验证步骤
+### 验证步骤(labelId)
1. 选定门店,确认当日已有 N 条 `fl_label_print_task`(可用 print-log 列表核对)。
2. 调用 **preview**,`labelId` 应为 `{今日yyyyMMdd}-{N+1}`。
3. 执行 **print** 创建新任务后,print-log 中该任务 `labelId` 与预览时一致(同一时刻连续预览+打印)。
4. 修改 `baseTime` 为历史日期,序号应按该日任务数计算,而非「今天」。
-### SQL 抽查
+#### SQL 抽查
```sql
SELECT Id, LocationId, PrintedAt, CreationTime
@@ -122,7 +119,206 @@ ORDER BY IFNULL(PrintedAt, CreationTime), Id;
---
+## 二、Company 自动生成元素(预览/打印填充)
+
+### 背景
+
+模板编辑器中的 **Company** 控件属于「按门店自动取数」类型。用户点击打印时,后端应:
+
+- **始终**展示当前门店所属 **Company 名称**(`fl_partner.PartnerName`)
+- **仅当模板元素 config 中勾选** Address / City / State / Zip / Email 时,才追加对应行;未勾选或库中无值则不展示该字段
+
+勾选状态**不落独立表**,保存在 **`fl_label_template_element.ConfigJson`**(通过 `POST/PUT /api/app/label-template` 的 `elements[].config` 写入)。公司主数据来自 **`fl_partner`**。
+
+> 本次**不提供** Web 属性面板 UI;需通过 **label-template 保存接口** 或 SQL 维护 `config`。
+
+### 元素识别规则
+
+满足以下任一条件即视为 Company 自动生成元素(`PartnerCompanyDisplayHelper.IsCompanyAutoElement`):
+
+| 条件 | 说明 |
+|------|------|
+| `typeAdd = "auto_Company"` | 标准 Company 控件 |
+| `valueSourceType = "AUTO_DB"` 且 `typeAdd` 以 `auto_` 开头且含 `Company` | 兼容命名 |
+
+常见组合:`elementType = "TEXT_STATIC"`,`valueSourceType = "AUTO_DB"`,`typeAdd = "auto_Company"`。
+
+### 模板 config(勾选字段)
+
+写入 `elements[].config`(JSON),支持两种等价写法(可混用):
+
+**方式 A:数组 `companyIncludeFields`**
+
+| 数组值 | 含义 | 数据来源(`fl_partner`) |
+|--------|------|--------------------------|
+| `address` | 街道地址 | `Street` |
+| `city` | 城市 | `City` |
+| `state` | 州/省 | `StateCode` |
+| `zip` | 邮编 | `ZipCode` |
+| `email` | 邮箱 | `ContactEmail` |
+
+**方式 B:布尔开关**
+
+`includeAddress` / `includeCity` / `includeState` / `includeZip` / `includeEmail`(`true` 表示勾选)
+
+**示例(保存模板时)**
+
+```json
+{
+ "elementType": "TEXT_STATIC",
+ "valueSourceType": "AUTO_DB",
+ "typeAdd": "auto_Company",
+ "name": "Company",
+ "config": {
+ "companyIncludeFields": ["address", "city", "state", "zip", "email"]
+ }
+}
+```
+
+或:
+
+```json
+"config": {
+ "includeAddress": true,
+ "includeCity": true,
+ "includeState": true,
+ "includeZip": true,
+ "includeEmail": false
+}
+```
+
+### 展示文本格式
+
+后端将解析结果写入 **`template.elements[].config.text`**(多行,`\n` 分隔):
+
+| 行序 | 内容 | 规则 |
+|------|------|------|
+| 第 1 行 | 公司名 | **固定输出**(有 `PartnerName` 时) |
+| 第 2 行 | 街道 | 仅勾选 `address` 且 `Street` 非空 |
+| 第 3 行 | 城市, 州, 邮编 | 勾选 city/state/zip 中任一项时,按「City, StateCode, ZipCode」逗号拼接(空段跳过) |
+| 第 4 行 | 邮箱 | 仅勾选 `email` 且 `ContactEmail` 非空 |
+
+**渲染示例**
+
+```
+Acme Foods Inc
+123 Main Street
+New York, NY, 10001
+sales@acme.com
+```
+
+若仅勾选公司名(无任何 include),则 `text` 仅一行公司名。
+
+### 门店 → Company 解析
+
+1. 入参 **`locationId`**(`location.Id`,Guid 字符串)
+2. 查 `location` 表取 **`Partner`** 字段
+3. 在 **`fl_partner`** 中按 **`Id = Partner`** 或 **`PartnerName = Partner`** 匹配(未删除)
+4. 匹配失败时不抛错,`config.text` 保持原样(不填充)
+
+### 入参要求
+
+| 场景 | `locationId` |
+|------|----------------|
+| 模板**不含** Company 自动生成元素 | 可选(App preview 仍必填门店,与原有逻辑一致) |
+| 模板**含** Company 自动生成元素 | **`LabelPreviewResolveInputVo.locationId` 必填**;缺失返回 **400**:`预览/打印需要 locationId 以填充 Company 信息` |
+
+**App 调用链**:`UsAppLabelPreviewInputVo.locationId` → 内部 `LabelAppService.PreviewAsync(LabelPreviewResolveInputVo)` 时须**原样传入** `locationId`。
+
+受影响接口(均通过 `LabelAppService.PreviewAsync` 渲染模板):
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/api/app/us-app-labeling/preview` | App 预览 |
+| POST | `/api/app/us-app-labeling/print` | App 打印(落库前解析模板) |
+| POST | `/api/app/label/preview`(或管理端等价预览) | Web 预览;含 Company 元素时 Body 须带 `locationId` |
+
+重打 **`reprint`** 使用历史任务 `RenderTemplateJson` 快照,**不再**重新解析 Company。
+
+### 出参变更
+
+无新增顶层字段;变更在 **`template.elements[]`** 内:
+
+| 字段 | 变更 |
+|------|------|
+| `elements[].config.text` | Company 元素由后端按上文规则写入多行文本 |
+
+### 数据库与脚本
+
+| 项 | 说明 |
+|----|------|
+| `fl_partner.PartnerName` | 公司名(必有) |
+| `fl_partner.Street` / `City` / `StateCode` / `ZipCode` / `ContactEmail` | 可选展示字段 |
+| `location.Partner` | 门店归属 Company 键(Id 或名称) |
+| DDL | `scripts/fl_partner_add_address_columns.sql`(缺地址列时执行) |
+
+模板适用范围(Company/Region/Location 三维 scope)见 **`项目相关文档/6-4代码优化.md`**,与本节「元素内 Company 自动填值」相互独立。
+
+### 请求示例(含 Company 的 preview)
+
+```bash
+curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \
+ -H "Authorization: Bearer {token}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "locationId": "550e8400-e29b-41d4-a716-446655440000",
+ "labelCode": "LBL0001",
+ "productId": "PROD001"
+ }'
+```
+
+响应 `template.elements` 中 Company 元素片段示例:
+
+```json
+{
+ "elementType": "TEXT_STATIC",
+ "typeAdd": "auto_Company",
+ "valueSourceType": "AUTO_DB",
+ "config": {
+ "companyIncludeFields": ["address", "city", "state", "zip"],
+ "text": "Acme Foods Inc\n123 Main Street\nNew York, NY, 10001"
+ }
+}
+```
+
+### 涉及代码(Company)
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/PartnerCompanyDisplayHelper.cs` | 识别元素、解析 config、格式化文本、门店→Partner |
+| `Services/LabelAppService.cs` | `PreviewAsync` 填充 Company 的 `config.text` |
+| `Dtos/Label/LabelPreviewResolveInputVo.cs` | 新增 `locationId` |
+| `Services/UsAppLabelingAppService.cs` | preview/print 调用预览时传入 `locationId` |
+
+### 验证步骤(Company)
+
+1. 确认门店 `location.Partner` 能关联到有效 `fl_partner` 记录(地址列已执行 DDL)。
+2. 模板含 `auto_Company` 元素,`config` 勾选若干 include 字段;保存后查 `fl_label_template_element.ConfigJson`。
+3. 调用 **preview**,Body 带正确 `locationId`:`template.elements` 中 Company 的 `config.text` 行数与勾选一致。
+4. 去掉 `locationId` 或传空 → 返回 400(仅当模板含 Company 元素时)。
+5. 执行 **print**,检查 `fl_label_print_task.RenderTemplateJson` 内 Company `text` 与预览一致。
+
+#### SQL 抽查
+
+```sql
+-- 门店归属 Company
+SELECT l.Id, l.Partner, p.PartnerName, p.Street, p.City, p.StateCode, p.ZipCode, p.ContactEmail
+FROM location l
+LEFT JOIN fl_partner p ON (p.Id = l.Partner OR p.PartnerName = l.Partner) AND p.IsDeleted = 0
+WHERE l.Id = '{locationId}' AND l.IsDeleted = 0;
+
+-- 模板 Company 元素 config
+SELECT e.Id, e.TypeAdd, e.ValueSourceType, e.ConfigJson
+FROM fl_label_template_element e
+JOIN fl_label_template t ON t.Id = e.TemplateId
+WHERE t.TemplateCode = '{templateCode}' AND e.IsDeleted = 0
+ AND e.TypeAdd = 'auto_Company';
+```
+
+---
+
## 关联文档
- 同序号规则:`项目相关文档/6-2代码优化.md` → **App `get-print-log-list` 的 Label ID**
+- 模板三维 scope:`项目相关文档/6-4代码优化.md` → **`/api/app/label-template`**
- App 预览页读取字段:`labelId` / `LabelId`(`preview.vue`)
diff --git a/项目相关文档/6-16代码优化.md b/项目相关文档/6-16代码优化.md
new file mode 100644
index 0000000..8e3a5ae
--- /dev/null
+++ b/项目相关文档/6-16代码优化.md
@@ -0,0 +1,139 @@
+# 6-16 代码优化
+
+本文档说明 **2026-06-16** 对美国版 Web 管理端 **`GET /api/app/dashboard/overview`** 中 **Recent Labels** 区块 **Label ID** 展示规则的同步改造。
+
+测试环境:`http://flus-test.3ffoodsafety.com`
+
+---
+
+## 背景
+
+Dashboard「Recent Labels」列表原先 `recentLabels[].labelCode` 返回 **`fl_label.LabelCode`**(或标签业务编码),与业务要求的 **Label ID** 含义不一致。
+
+业务规则(与 App preview、Print Log、报表一致):
+
+**Label ID = 某门店在某个自然日内,每次打印任务按时间递增的序号**
+
+| 示例 | 含义 |
+|------|------|
+| `20260513-1` | 该门店 2026-05-13 当日第 1 次打印 |
+| `20260513-2` | 同日第 2 次 |
+| `20260514-1` | 次日重新从 1 计数 |
+
+格式:`{yyyyMMdd}-{n}`(`n` 从 1 递增;按 **`PrintedAt ?? CreationTime`** 所在自然日、同一 **`locationId`** 统计)。
+
+> **注意**:`labelCode` 字段名保持不变(前端 Recent Labels 已绑定该字段展示 Label ID),**语义**由「标签编码」改为「门店当日打印序号」。不是 `fl_label.Id`,也不是 `fl_label.LabelCode`。
+
+---
+
+## 接口说明
+
+| 项目 | 内容 |
+|------|------|
+| 方法 | `GET` |
+| 路径 | `/api/app/dashboard/overview` |
+| 鉴权 | Bearer Token(Web 管理端登录 Token) |
+| 入参 | 无 |
+
+### 出参变更(`DashboardOverviewOutputDto.recentLabels[]`)
+
+| 字段 | 变更前 | 变更后 |
+|------|--------|--------|
+| **`labelCode`** | `fl_label.LabelCode`(标签业务编码) | 该打印任务在所属门店、打印日内的序号 **`yyyyMMdd-n`** |
+
+`recentLabels` 其余字段(`taskId`、`displayName`、`printedByName`、`printedAt`、`status`、`labelTypeBadge`)不变。
+
+Overview 其它区块(指标卡片、周趋势、分类分布等)不受影响。
+
+### `labelCode`(Label ID)计算规则
+
+与 **`POST /api/app/us-app-labeling/preview`**、**`GET /api/app/reports/print-log-list`** 共用 Helper:**`ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync`**。
+
+1. 取打印任务 **`PrintedAt ?? CreationTime`** 的日期部分 `yyyyMMdd`。
+2. 在同一 **`locationId`**、同一自然日内,按 `PrintedAt ?? CreationTime` 升序、`Id` 升序对所有任务排序。
+3. 第 `i` 条任务的 Label ID = **`{yyyyMMdd}-{i}`**(`i` 从 1 开始)。
+4. Recent Labels 取权限范围内**最新 10 条**打印任务;每条任务的 `labelCode` 为其在**所属门店当日序列**中的序号(非全平台统一编号)。
+
+多门店场景:不同门店各自从 `-1` 计数;同一 Dashboard 列表可混合展示多个门店的记录,每条记录的 `labelCode` 仅对应该任务的 `locationId` + 打印日。
+
+---
+
+## 请求示例
+
+```bash
+curl -X GET "http://flus-test.3ffoodsafety.com/api/app/dashboard/overview" \
+ -H "Authorization: Bearer {token}"
+```
+
+### 响应片段(`recentLabels` 示例)
+
+```json
+{
+ "recentLabels": [
+ {
+ "taskId": "1234567890123456789",
+ "labelCode": "20260604-3",
+ "displayName": "Organic Milk 1L",
+ "printedByName": "Alice",
+ "printedAt": "2026-06-04T14:22:00",
+ "status": "active",
+ "labelTypeBadge": "2\"x2\""
+ },
+ {
+ "taskId": "1234567890123456788",
+ "labelCode": "20260604-2",
+ "displayName": "Whole Wheat Bread",
+ "printedByName": "Bob",
+ "printedAt": "2026-06-04T11:05:00",
+ "status": "expired",
+ "labelTypeBadge": "4\"x2\""
+ }
+ ],
+ "generatedAt": "2026-06-04T15:00:00"
+}
+```
+
+若某任务无法解析门店或打印时间,则 `labelCode` 为 **`无`**。
+
+---
+
+## 涉及代码
+
+| 文件 | 说明 |
+|------|------|
+| `Services/DashboardAppService.cs` | `GetOverviewAsync`:`recentLabels` 查询增加 `LocationId`,调用 `ResolveDailyLabelIdsAsync` 填充 `labelCode` |
+| `Dtos/Dashboard/DashboardRecentLabelItemDto.cs` | `LabelCode` XML 注释更新为当日序号语义 |
+| `IServices/IDashboardAppService.cs` | 接口 `remarks` 补充 `recentLabels[].labelCode` 规则说明 |
+| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | 共用(与 6-11 preview / 6-2 print-log 一致) |
+
+---
+
+## 验证步骤
+
+1. 登录 Web 管理端,获取 Bearer Token。
+2. 选定门店,确认某自然日已有 N 条 `fl_label_print_task`(可用 print-log 或 SQL 核对)。
+3. 调用 **`GET /api/app/dashboard/overview`**,`recentLabels` 中对应任务的 **`labelCode`** 应与 print-log 列表中同一 `taskId` 的 Label ID 一致。
+4. 跨日打印:次日首条应为 `{新日期}-1`。
+5. 多门店:两门店同日各打印 1 次,两条记录的 `labelCode` 末尾序号均可为 `-1`(各自门店独立计数)。
+
+### SQL 抽查
+
+```sql
+SELECT Id, LocationId, PrintedAt, CreationTime,
+ IFNULL(PrintedAt, CreationTime) AS EffectiveTime
+FROM fl_label_print_task
+WHERE LocationId = '{locationId}'
+ AND IFNULL(PrintedAt, CreationTime) >= '{yyyy-MM-dd}'
+ AND IFNULL(PrintedAt, CreationTime) < DATE_ADD('{yyyy-MM-dd}', INTERVAL 1 DAY)
+ORDER BY EffectiveTime, Id;
+```
+
+按行号 `1..N` 对应 `labelCode` 的 `-n` 部分;日期部分为 `yyyyMMdd`。
+
+---
+
+## 关联文档
+
+- Label ID 通用规则与 preview:`项目相关文档/6-11代码优化.md`
+- App print-log Label ID:`项目相关文档/6-2代码优化.md`
+- Dashboard 数据范围(管理员 / 门店绑定):`DashboardScopeHelper` 及接口 `IDashboardAppService` 注释
diff --git a/项目相关文档/6-17代码优化.md b/项目相关文档/6-17代码优化.md
new file mode 100644
index 0000000..337ab5a
--- /dev/null
+++ b/项目相关文档/6-17代码优化.md
@@ -0,0 +1,336 @@
+# 6-17 代码优化
+
+本文档说明 **2026-06-17** 对美国版的两项后端改造:
+
+1. **`GET /api/app/label-template`** 列表 500 修复(scope 库结构兼容)
+2. **`POST /api/app/us-app-labeling/get-print-log-list`** 增加 **按自然日** 的打印时间筛选
+
+测试环境:`http://flus-test.3ffoodsafety.com`
+
+---
+
+## 一、label-template 列表修复
+
+Web 管理端 **Label Templates** 页加载失败,Network 中:
+
+`GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 返回错误,页面提示 **Failed to load label templates**。
+
+### 根因
+
+6-4 改造引入 **Company / Region / Location** 三维适用范围后,代码会:
+
+1. 查询 `fl_label_template.AppliedPartnerType` / `AppliedRegionType`
+2. 关联 `fl_label_template_partner` / `fl_label_template_region`
+
+测试库若**尚未执行**迁移脚本 `fl_label_template_scope.sql`,上述列/表不存在,SqlSugar 生成 SQL 时报 **Unknown column / Table doesn't exist**,接口 500。
+
+> `SkipCount=1` 表示**第 1 页**(页码从 1 起),不是报错原因。
+
+---
+
+### 修复说明
+
+#### 1. 库结构兼容(未迁移仍可列表)
+
+| 改动 | 说明 |
+|------|------|
+| `LabelTemplateScopeSchemaHelper` | 启动后首次请求检测 partner/region 关联表是否存在 |
+| `FlLabelTemplateDbEntity` | `AppliedPartnerType` / `AppliedRegionType` 标记 `IsIgnore`,避免 SELECT 不存在列 |
+| `LabelTemplateScopeHelper` | 未迁移时列表/详情 **仅 Location 维度** 过滤与展示;Company/Region 显示 `All Companies` / `All Regions` |
+| 已迁移库 | 行为与 6-4 一致,按关联表判断 ALL/SPECIFIED |
+
+#### 2. 列表筛选补全
+
+`LabelTemplateGetListInputVo` 增加 **`partnerId`**(Company),与 `groupId`、`locationId` 按「门店优先」解析(`ResolveFilteredLocationIdsForListAsync`)。
+
+#### 3. Items 列
+
+`LabelTemplateListItemsHelper` 补全 `ElementKey` 查询字段,控件名称排序稳定。
+
+---
+
+### 接口说明(label-template)
+
+| 项目 | 内容 |
+|------|------|
+| 方法 | `GET` |
+| 路径 | `/api/app/label-template` |
+| 鉴权 | Bearer Token |
+
+### 入参(Query)
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `SkipCount` | int | 否 | **页码,从 1 起**;第一页传 `1`(与全平台列表约定一致) |
+| `MaxResultCount` | int | 否 | 每页条数,默认 10 |
+| `Keyword` | string | 否 | 模板名称 / 编码模糊搜索 |
+| `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 |
+| `GroupId` | string | 否 | 按 Region(`fl_group.Id`)筛选 |
+| `LocationId` | string | 否 | 按门店(`location.Id`)筛选;**优先于** Partner/Region |
+| `LabelType` | string | 否 | 如 `PRICE` / `NUTRITION` |
+| `State` | bool | 否 | 启用状态 |
+| `Sorting` | string | 否 | 排序字段 |
+
+筛选解析顺序:**LocationId → GroupId → PartnerId**;均未传则不按门店范围收窄(仍受登录账号数据权限约束)。
+
+### 出参(`PagedResultWithPageDto`)
+
+| 字段 | 说明 |
+|------|------|
+| `pageIndex` | 当前页码 |
+| `pageSize` | 每页条数 |
+| `totalCount` | 总条数 |
+| `totalPages` | 总页数 |
+| `items[]` | 模板列表 |
+
+**`items[]` 主要字段**
+
+| 字段 | 说明 |
+|------|------|
+| `id` / `templateCode` | 模板编码(前端主键) |
+| `templateName` | 模板名称 |
+| `company` / `region` / `location` | 三维适用范围展示文案 |
+| `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组(ALL 时为空) |
+| `items` / `itemNames` | 模板内控件名称(逗号拼接 / 数组) |
+| `contentsCount` | 控件数量 |
+| `sizeText` | 如 `4x2inch` |
+| `lastEdited` | 最近编辑时间 |
+
+---
+
+### 请求示例(label-template)
+
+```bash
+curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
+ -H "Authorization: Bearer {token}" \
+ --data-urlencode "SkipCount=1" \
+ --data-urlencode "MaxResultCount=10"
+```
+
+带筛选:
+
+```bash
+curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
+ -H "Authorization: Bearer {token}" \
+ --data-urlencode "SkipCount=1" \
+ --data-urlencode "MaxResultCount=10" \
+ --data-urlencode "PartnerId=1234567890123456789" \
+ --data-urlencode "Keyword=Price"
+```
+
+### 响应片段(示例)
+
+```json
+{
+ "pageIndex": 1,
+ "pageSize": 10,
+ "totalCount": 2,
+ "totalPages": 1,
+ "items": [
+ {
+ "id": "TPL-PRICE-01",
+ "templateCode": "TPL-PRICE-01",
+ "templateName": "Standard Price Label",
+ "company": "All Companies",
+ "region": "All Regions",
+ "location": "All Locations",
+ "items": "Label Name, Price, Barcode",
+ "itemNames": ["Label Name", "Price", "Barcode"],
+ "contentsCount": 3,
+ "sizeText": "2x2inch",
+ "lastEdited": "2026-06-04T10:00:00"
+ }
+ ]
+}
+```
+
+---
+
+## 数据库迁移(启用完整三维 scope)
+
+未执行前:列表可正常返回,但 **无法** 保存 Company/Region 多选明细。
+
+**脚本(可重复执行)**:
+
+`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_template_scope.sql`
+
+| 对象 | 说明 |
+|------|------|
+| `fl_label_template.AppliedPartnerType` | Company:ALL / SPECIFIED |
+| `fl_label_template.AppliedRegionType` | Region:ALL / SPECIFIED |
+| `fl_label_template_partner` | Company 多选明细 |
+| `fl_label_template_region` | Region 多选明细 |
+
+执行后重启应用,新建/编辑模板即可写入 Company/Region 范围。
+
+### SQL 检查是否已迁移
+
+```sql
+SELECT COLUMN_NAME
+FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'fl_label_template'
+ AND COLUMN_NAME IN ('AppliedPartnerType', 'AppliedRegionType');
+
+SELECT TABLE_NAME
+FROM information_schema.TABLES
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME IN ('fl_label_template_partner', 'fl_label_template_region');
+```
+
+---
+
+### 涉及代码(label-template)
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 检测 scope 表是否已迁移 |
+| `Helpers/LabelTemplateScopeHelper.cs` | 兼容未迁移库的过滤/展示/保存 |
+| `Helpers/LabelTemplateListItemsHelper.cs` | Items 列 ElementKey |
+| `Services/DbModels/FlLabelTemplateDbEntity.cs` | Partner/Region 列 IsIgnore |
+| `Services/LabelTemplateAppService.cs` | 列表筛选支持 `partnerId` |
+| `Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs` | 新增 `PartnerId` |
+| `IServices/ILabelTemplateAppService.cs` | 接口注释 |
+
+### 验证步骤(label-template)
+
+1. **未迁移库**:仅执行后端部署,调用 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 应 **200**,Web 列表可加载。
+2. **已迁移库**:列表 `company`/`region` 展示与 6-4 一致;指定 Company 保存后再查列表 Id 数组正确。
+3. 分页:第 1 页 `SkipCount=1`,第 2 页 `SkipCount=2`,`totalCount` 与 UI 分页一致。
+4. 筛选:传 `LocationId` 后仅返回该门店可见模板 + 全范围模板。
+
+---
+
+## 二、App Print Log 按日筛选(get-print-log-list)
+
+### 背景
+
+App **Print Log** 页增加 **Date** 选择器(及 **Today** 快捷按钮),需按所选自然日只展示该门店当日的打印记录;无记录时提示如 `No print records for 06/16/2026`。
+
+改造前接口仅按门店 + 权限分页,**无日期条件**,无法与 UI 日期筛选对齐。
+
+### 接口说明
+
+| 项目 | 内容 |
+|------|------|
+| 方法 | `POST` |
+| 路径 | `/api/app/us-app-labeling/get-print-log-list` |
+| 鉴权 | App Bearer Token |
+
+### 入参(Body:`PrintLogGetListInputVo`)
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `locationId` | string | 是 | 当前门店 Id(`location.Id`) |
+| **`printDate`** | DateTime | 否 | **打印日期(自然日)**;按 `PrintedAt ?? CreationTime` 落在该日的任务筛选 |
+| `skipCount` | int | 否 | 页码,从 **1** 起 |
+| `maxResultCount` | int | 否 | 每页条数 |
+
+#### `printDate` 规则
+
+1. **未传** → 默认 **服务器当天**(与 App 默认选中 Today 一致)。
+2. **传入任意时刻** → 仅取 **日期部分**(`Date`),区间为 `[当天 00:00:00, 次日 00:00:00)`。
+3. 有效时间字段:`fl_label_print_task.PrintedAt` 优先,为空则用 `CreationTime`(与 Label ID 序号、`6-2` / `6-11` 一致)。
+4. 权限不变:管理员 / Partner 看门店全部;其它角色仅本人(`CreatedBy`)。
+
+### 出参
+
+仍为 `PagedResultWithPageDto`;`items[]` 字段不变。`totalCount` 为**该日**符合条件的总数。
+
+| 字段 | 说明 |
+|------|------|
+| `labelId` | 门店当日打印序号 `yyyyMMdd-n`(与 preview / 报表一致) |
+| `printedAt` | 打印时间 |
+| 其它 | 与改造前一致 |
+
+### 请求示例
+
+**查询当天(显式传 printDate)**
+
+```bash
+curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/get-print-log-list" \
+ -H "Authorization: Bearer {token}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "locationId": "550e8400-e29b-41d4-a716-446655440000",
+ "printDate": "2026-06-16T00:00:00",
+ "skipCount": 1,
+ "maxResultCount": 20
+ }'
+```
+
+**Today 按钮(不传 printDate,后端默认当天)**
+
+```json
+{
+ "locationId": "550e8400-e29b-41d4-a716-446655440000",
+ "skipCount": 1,
+ "maxResultCount": 20
+}
+```
+
+### 响应片段(示例)
+
+```json
+{
+ "pageIndex": 1,
+ "pageSize": 20,
+ "totalCount": 2,
+ "totalPages": 1,
+ "items": [
+ {
+ "taskId": "1234567890123456789",
+ "labelId": "20260616-1",
+ "productName": "Organic Milk",
+ "printedAt": "2026-06-16T09:15:00",
+ "operatorName": "Alice",
+ "locationName": "Ordos Airport"
+ },
+ {
+ "taskId": "1234567890123456788",
+ "labelId": "20260616-2",
+ "printedAt": "2026-06-16T14:30:00"
+ }
+ ]
+}
+```
+
+若 `totalCount = 0`,App 可展示 `No print records for MM/DD/YYYY`。
+
+### 涉及代码(get-print-log-list)
+
+| 文件 | 说明 |
+|------|------|
+| `Dtos/UsAppLabeling/PrintLogGetListInputVo.cs` | 新增 `PrintDate` |
+| `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` 增加日期 Where |
+| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | 新增 `ResolvePrintLogFilterDayRange` |
+| `IServices/IUsAppLabelingAppService.cs` | 接口注释 |
+
+### 验证步骤(get-print-log-list)
+
+1. 门店在 `2026-06-16` 有 N 条打印任务,传 `printDate=2026-06-16` → `totalCount=N`,且每条 `printedAt` 落在该日。
+2. 传 `printDate=2026-06-15` 且无记录 → `totalCount=0`,`items=[]`。
+3. 不传 `printDate` → 等价于查询**服务器当天**。
+4. 同一 `printDate` 下 `labelId` 序号与 `6-2` 规则一致(`-1`、`-2` … 按当日时间升序)。
+
+#### SQL 抽查
+
+```sql
+SELECT Id, LocationId, PrintedAt, CreationTime
+FROM fl_label_print_task
+WHERE LocationId = '{locationId}'
+ AND IFNULL(PrintedAt, CreationTime) >= '2026-06-16'
+ AND IFNULL(PrintedAt, CreationTime) < '2026-06-17'
+ORDER BY IFNULL(PrintedAt, CreationTime), Id;
+```
+
+行数应与接口 `totalCount`(该日、该门店、权限范围内)一致。
+
+---
+
+## 关联文档
+
+- 三维 scope 业务规则:`项目相关文档/6-4代码优化.md`
+- App Print Log Label ID:`项目相关文档/6-2代码优化.md`
+- Preview 当日序号:`项目相关文档/6-11代码优化.md`
+- 分页约定(SkipCount = 页码):`Helpers/PagedQueryConvention.cs`
diff --git a/项目相关文档/6-18代码优化.md b/项目相关文档/6-18代码优化.md
new file mode 100644
index 0000000..9f5fe00
--- /dev/null
+++ b/项目相关文档/6-18代码优化.md
@@ -0,0 +1,650 @@
+# 6-18 代码优化
+
+本文档说明 **2026-06-18** 对 **`GET /api/app/label-template`** 列表接口的第二轮修复。
+
+6-17 已完成 scope 库结构兼容(未迁移 `fl_label_template_partner` / `fl_label_template_region` 时仍可查询),但测试环境 Web **Label Templates** 页仍返回 **500**,Network 中:
+
+`GET /api/app/label-template?SkipCount=1&MaxResultCount=10`
+
+测试环境:`http://flus-test.3ffoodsafety.com`
+
+---
+
+## 一、现象
+
+- 页面提示:**Failed to load label templates. Request failed.**
+- 接口 HTTP **500**,响应体 `errors` 为空,无具体异常文案。
+- 同页 `partner` / `group` / `location` 下拉接口可正常加载。
+
+> `SkipCount=1` 表示**第 1 页**(页码从 1 起),不是报错原因。详见 `Helpers/PagedQueryConvention.cs`。
+
+---
+
+## 二、根因
+
+服务端日志已明确报错:
+
+```text
+Unknown column 'AppliedPartnerType' in 'field list'
+```
+
+对应 SQL:
+
+```sql
+SELECT ... `AppliedLocationType`,`AppliedPartnerType`,`AppliedRegionType`, ...
+FROM `fl_label_template`
+WHERE NOT ( `IsDeleted`=1 )
+ORDER BY IFNULL(`LastModificationTime`,`CreationTime`) DESC
+LIMIT 0,10
+```
+
+### 2.1 主因:ORM 实体仍映射不存在列
+
+6-17 曾在 `FlLabelTemplateDbEntity` 上对 `AppliedPartnerType` / `AppliedRegionType` 标记 `[SugarColumn(IsIgnore = true)]`,但当前 SqlSugar 运行时**仍会**把这两列拼进 `SELECT`(与 `fl_label.AppliedRegionType` 的 `IsIgnore` 行为不一致),导致未执行 `fl_label_template_scope.sql` 的库直接 500。
+
+**MCP 查库(测试库)**:
+
+- `fl_label_template` **无** `AppliedPartnerType` / `AppliedRegionType` 列
+- **无** `fl_label_template_partner` / `fl_label_template_region` 表
+- **有** `AppliedLocationType` 与 `fl_label_template_location`
+
+### 2.2 次因:列表 SQL 其它隐患(一并修复)
+
+| # | 问题 | 后果 |
+|---|------|------|
+| 1 | 默认排序曾用 `LastModificationTime ?? CreationTime` | SqlSugar 翻译失败 → 500 |
+| 2 | `Sorting` 直接 `OrderBy(input.Sorting)` 拼 SQL | 非法字段 / 注入风险 |
+| 3 | 空 `templateIds` 仍 `Contains` 查询 | 可能生成 `IN ()` |
+| 4 | 无效 partner/group 筛选未安全降级 | 未捕获异常 |
+
+---
+
+## 三、修复说明
+
+### 1. 从 ORM 实体移除不存在列 + 强制列投影(核心)
+
+| 改动 | 说明 |
+|------|------|
+| `FlLabelTemplateDbEntity` | **删除** `AppliedPartnerType` / `AppliedRegionType` 属性 |
+| **`LabelTemplateQueryHelper.ProjectListColumns`** | 列表/详情/重复校验等只读查询 **显式 Select** 真实列,SQL 不再出现 scope 列 |
+| `LabelTemplateScopeSchemaHelper` | 探测列/表;已迁移时用 raw SQL 写入 scope 列 |
+| `LabelTemplateAppService` | `GetListAsync` 在排序后调用 `ProjectListColumns` |
+
+> **重要**:若 Yi-SQL 日志仍出现 `AppliedPartnerType`,说明进程加载的是**旧 DLL**(`FoodLabeling.Application` 未重新编译或未重启)。请先 `dotnet build` 通过后再**完全停止并重启** `Yi.Abp.Web`。
+
+### 2. 安全排序
+
+新增 `ApplyLabelTemplateListSorting`:
+
+- 默认:`ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC`
+- `Sorting` 白名单字段 + asc/desc
+
+### 3. 列表查询健壮性
+
+| 改动 | 说明 |
+|------|------|
+| `input ??= new()`、`pageSize` 默认 10 | 入参/分页兜底 |
+| `templateIds.Count > 0` 再查 elements/items | 避免空 `IN ()` |
+| `ResolveFilteredLocationIdsForListAsync` | 无效 partner/group 返回空列表 |
+| `DeleteAsync` | 仅当 scope 关联表存在时才删除 partner/region 行 |
+
+### 4. 与 6-17 的关系
+
+6-17 用 `IsIgnore` 试图跳过列映射,**实测无效**;6-18 改为**实体不含列 + raw SQL 按需写入**,与 `fl_label.AppliedRegionType` 处理方式一致。
+
+---
+
+## 四、接口说明
+
+| 项目 | 内容 |
+|------|------|
+| 方法 | `GET` |
+| 路径 | `/api/app/label-template` |
+| 鉴权 | Bearer Token(`Authorization: {data.token}`,`data.token` 已含 `Bearer ` 前缀) |
+
+### 入参(Query)
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `SkipCount` | int | 否 | **页码,从 1 起**;第一页传 `1` |
+| `MaxResultCount` | int | 否 | 每页条数;`<=0` 时后端按 **10** 处理 |
+| `Keyword` | string | 否 | 模板名称 / 编码模糊搜索 |
+| `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 |
+| `GroupId` | string | 否 | 按 Region(`fl_group.Id`)筛选 |
+| `LocationId` | string | 否 | 按门店(`location.Id`)筛选;**优先于** Partner/Region |
+| `LabelType` | string | 否 | 如 `PRICE` / `NUTRITION` |
+| `State` | bool | 否 | 启用状态 |
+| `Sorting` | string | 否 | 白名单:`TemplateName asc/desc`、`TemplateCode asc/desc`、`CreationTime asc/desc`、`LastModificationTime asc/desc`;其它值忽略并走默认排序 |
+
+筛选解析顺序:**LocationId → GroupId → PartnerId**;均未传则不按门店范围收窄(仍返回全部未删除模板,除非前端传了无效 Id 则返回空列表)。
+
+### 出参(`PagedResultWithPageDto`)
+
+| 字段 | 说明 |
+|------|------|
+| `pageIndex` | 当前页码 |
+| `pageSize` | 每页条数 |
+| `totalCount` | 总条数 |
+| `totalPages` | 总页数 |
+| `items[]` | 模板列表 |
+
+**`items[]` 主要字段**
+
+| 字段 | 说明 |
+|------|------|
+| `id` / `templateCode` | 模板编码(前端主键) |
+| `templateName` | 模板名称 |
+| `company` / `region` / `location` | 适用范围展示;未迁移 scope 库时 Company/Region 为 `All Companies` / `All Regions` |
+| `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组 |
+| `items` / `itemNames` | 模板内控件名称 |
+| `contentsCount` | 控件数量 |
+| `sizeText` | 如 `2x2inch` |
+| `lastEdited` | 最近编辑时间(`LastModificationTime ?? CreationTime`) |
+
+---
+
+## 五、请求示例
+
+### 登录获取 Token
+
+```bash
+curl -X POST "http://flus-test.3ffoodsafety.com/api/oauth/Login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "userName=admin&password=123456"
+```
+
+### 列表(第一页,默认排序)
+
+```bash
+curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
+ -H "Authorization: Bearer {token}" \
+ --data-urlencode "SkipCount=1" \
+ --data-urlencode "MaxResultCount=10"
+```
+
+### 带 Company 筛选
+
+```bash
+curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
+ -H "Authorization: Bearer {token}" \
+ --data-urlencode "SkipCount=1" \
+ --data-urlencode "MaxResultCount=10" \
+ --data-urlencode "PartnerId={fl_partner.Id}"
+```
+
+### 指定排序
+
+```bash
+curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
+ -H "Authorization: Bearer {token}" \
+ --data-urlencode "SkipCount=1" \
+ --data-urlencode "MaxResultCount=10" \
+ --data-urlencode "Sorting=TemplateName asc"
+```
+
+### 响应片段(示例)
+
+```json
+{
+ "pageIndex": 1,
+ "pageSize": 10,
+ "totalCount": 3,
+ "totalPages": 1,
+ "items": [
+ {
+ "id": "tpl_n0b5h9_mpyssitm",
+ "templateCode": "tpl_n0b5h9_mpyssitm",
+ "templateName": "Retail Label w/Price Copy",
+ "company": "All Companies",
+ "region": "All Regions",
+ "location": "Ordos Airport, Store B",
+ "items": "Label Name, Price, Barcode",
+ "contentsCount": 5,
+ "sizeText": "2x2inch",
+ "lastEdited": "2026-06-04T08:30:00"
+ }
+ ]
+}
+```
+
+---
+
+## 六、验证步骤
+
+0. **重新编译并重启**(必做):
+ ```bash
+ cd "美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web"
+ dotnet build
+ ```
+ 停止正在运行的 Web 进程后重新启动;确认 Yi-SQL 中 **不再出现** `AppliedPartnerType`。
+
+1. **部署**包含 6-17 + 6-18 的后端并重启。
+2. 调用 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` → 应 **200**,`totalCount >= 0`。
+3. Web **Label Templates** 列表可加载,不再出现红色 **Failed to load label templates**。
+4. 传无效 `PartnerId` / `GroupId` → **200** 且 `items=[]`(非 500)。
+5. 不传 `Sorting` → 按最近编辑时间降序;传 `Sorting=TemplateName asc` → 按名称升序。
+6. 分页:第 2 页 `SkipCount=2`,`totalCount` 与 UI 一致。
+
+### SQL 抽查(修复后 ORM 应生成的列)
+
+```sql
+SELECT Id, TemplateCode, TemplateName, AppliedLocationType,
+ IFNULL(LastModificationTime, CreationTime) AS LastEdited
+FROM fl_label_template
+WHERE IsDeleted = 0
+ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC
+LIMIT 10;
+```
+
+**不应再出现** `AppliedPartnerType` / `AppliedRegionType`。
+
+### 检查 scope 是否已迁移
+
+```sql
+SELECT COUNT(*) AS scope_table_cnt
+FROM information_schema.TABLES
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME IN ('fl_label_template_partner', 'fl_label_template_region');
+```
+
+`scope_table_cnt = 0` 时行为与 6-17 一致:仅 Location 维度落库与展示。
+
+---
+
+## 七、涉及代码
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/LabelTemplateQueryHelper.cs` | **新增** `ProjectListColumns` 强制 SQL 列白名单 |
+| `Services/DbModels/FlLabelTemplateDbEntity.cs` | **移除** Partner/Region 列属性 |
+| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 列/表探测 + `SetAppliedScopeTypesAsync` raw SQL |
+| `Services/LabelTemplateAppService.cs` | 安全排序、Create/Update 写 scope 列、列表健壮性 |
+| `Helpers/LabelTemplateScopeHelper.cs` | 未迁移库 scope 过滤/展示(6-17) |
+| `Helpers/LocationScopeBindingHelper.cs` | `ResolveFilteredLocationIdsForListAsync` |
+| `Helpers/LabelTemplateListItemsHelper.cs` | Items 列 |
+| `Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs` | `PartnerId` 筛选 |
+
+---
+
+## 八、数据库迁移(可选)
+
+需完整 Company / Region 三维 scope 时,执行:
+
+`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_template_scope.sql`
+
+未执行前:**列表可正常返回**(6-17 + 6-18),但无法持久化 Company/Region 多选明细。
+
+---
+
+## 九、App 标签预览 `POST /api/app/us-app-labeling/preview`
+
+### 现象
+
+App 标签预览页调用 preview 失败(500 或 400),常与 **label-template 列表** 同源:加载模板头时 ORM 仍 SELECT 不存在的 `AppliedPartnerType` / `AppliedRegionType`。
+
+另有两项逻辑缺陷(与 `6-11` 文档不一致):
+
+| # | 问题 | 后果 |
+|---|------|------|
+| 1 | `LabelAppService.PreviewAsync` 全表查询 `FlLabelTemplateDbEntity` | 未迁移 scope 列时 **Unknown column** → 500 |
+| 2 | `UsAppLabelingAppService.PreviewAsync` 未向 `_labelAppService.PreviewAsync` 传 **`locationId`** | 模板含 Company 自动生成元素时报 **「预览/打印需要 locationId 以填充 Company 信息」** |
+| 3 | 出参 **`labelId`** 仍返回 `fl_label.Id`(GUID) | 与 Print Log 当日序号 `yyyyMMdd-n` 不一致 |
+
+### 修复说明
+
+| 改动 | 说明 |
+|------|------|
+| `LabelAppService.PreviewAsync` | 模板头查询改用 `LabelTemplateQueryHelper.ProjectListColumns` |
+| `UsAppLabelingAppService.PreviewAsync` | 传入 `LocationId`;`labelId` 改用 `ReportsPrintLogDailyLabelIdHelper.ResolveNextDailyLabelIdAsync` |
+| `UsAppLabelingAppService.PrintAsync` | 解析模板时同步传入 `LocationId`(打印与预览 Company 填充一致) |
+| `DashboardAppService` | 模板统计 Count 同样走 `ProjectListColumns` |
+
+### 接口说明
+
+| 项目 | 内容 |
+|------|------|
+| 方法 | `POST` |
+| 路径 | `/api/app/us-app-labeling/preview` |
+| 鉴权 | App Bearer Token |
+
+#### 入参(Body:`UsAppLabelPreviewInputVo`)
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `locationId` | string | 是 | 当前门店 Id(`location.Id`) |
+| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) |
+| `productId` | string | 否 | 预览产品 Id;不传则取标签绑定第一个产品 |
+| `baseTime` | DateTime | 否 | 日期/时间控件基准;也用于计算当日 `labelId` 序号;未传为服务器当前时间 |
+| `printInputJson` | object | 否 | `PRINT_INPUT` 元素用户输入 |
+
+#### 出参(`UsAppLabelPreviewDto`)
+
+| 字段 | 说明 |
+|------|------|
+| **`labelId`** | 门店当日**下一个**打印序号 `yyyyMMdd-n`(预览不落库) |
+| `locationId` / `labelCode` | 回传入参 |
+| `template` | 已解析 AUTO_DB / PRINT_INPUT 的模板结构(含 Company 自动填充) |
+| `labelLastEdited` | 标签最近编辑时间 |
+| 其它 | `typeName`、`productName`、`templateProductDefaultValues` 等 |
+
+#### `labelId` 规则
+
+与 `6-11`、`get-print-log-list` 一致:`{baseTime 日期 yyyyMMdd}-{当日已有打印任务数 + 1}`。
+
+#### Company 自动元素
+
+模板含 Company 自动生成控件时,后端根据 **`locationId`** 查 `fl_partner` 填充 `config.text`(详见 `6-11` 第二节)。
+
+### 请求示例
+
+```bash
+curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \
+ -H "Authorization: Bearer {token}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "locationId": "550e8400-e29b-41d4-a716-446655440000",
+ "labelCode": "LBL0001",
+ "productId": "PROD001",
+ "baseTime": "2026-06-18T09:00:00"
+ }'
+```
+
+### 响应片段(示例)
+
+```json
+{
+ "labelId": "20260618-3",
+ "locationId": "550e8400-e29b-41d4-a716-446655440000",
+ "labelCode": "LBL0001",
+ "labelLastEdited": "2026-06-01T08:30:09",
+ "template": {
+ "id": "tpl_xxx",
+ "width": 2,
+ "height": 2,
+ "unit": "inch",
+ "elements": []
+ }
+}
+```
+
+### 验证步骤
+
+1. 停服 → `dotnet build` → 重启(同 label-template 一节)。
+2. App 进入标签预览页,Network 中 preview 应 **200**。
+3. 响应 `labelId` 为 `yyyyMMdd-n`,非 GUID。
+4. 模板含 Company 元素时,`template.elements` 中对应 `config.text` 为门店所属公司名。
+5. 当日已有 N 条打印任务时,preview 返回 `-{N+1}`。
+
+### 涉及代码(preview)
+
+| 文件 | 说明 |
+|------|------|
+| `Services/UsAppLabelingAppService.cs` | `PreviewAsync` / `PrintAsync` 传 `LocationId`、当日 `labelId` |
+| `Services/LabelAppService.cs` | `PreviewAsync` 模板头 `ProjectListColumns` |
+| `Helpers/LabelTemplateQueryHelper.cs` | 列投影 |
+| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolveNextDailyLabelIdAsync` |
+| `Helpers/PartnerCompanyDisplayHelper.cs` | Company 自动填充 |
+
+---
+
+## 十、App 打印日志 `POST /api/app/us-app-labeling/get-print-log-list`
+
+### 现象
+
+Postman 传 `printDate: "2026-06-16"` 返回 `totalCount: 0`,误以为接口异常。
+
+### 根因(查库核对)
+
+门店 `3a218397-8dda-a378-e024-ef89bcef8d24` 在测试库中:
+
+| 自然日(`DATE(IFNULL(PrintedAt, CreationTime))`) | 记录数 |
+|---------------------------------------------------|--------|
+| `2026-06-01` | **7** |
+| `2026-05-31` | **3** |
+| `2026-06-16` | **0** |
+
+**接口按入参日期筛选时,该日无数据则返回空列表,行为正确。** 文档/示例误用 `2026-06-16`,应改用 **`2026-06-01`** 验证。
+
+另:`PrintedAt` 入库多为 `null`,筛选实际走 **`CreationTime`**。
+
+### 本轮修复
+
+| 改动 | 说明 |
+|------|------|
+| 日期条件 | 改用 MySQL `DATE(IFNULL(t.PrintedAt, t.CreationTime)) = 'yyyy-MM-dd'`,避免 DateTime 区间比较时区偏差 |
+| `printDateDay` | 新增字符串入参(`yyyy-MM-dd`),优先于 `printDate`,避免 JSON 仅日期 UTC 歧义 |
+| 未传日期 | **`printDate` 与 `printDateDay` 均未传时不按日过滤**(返回该门店全部打印记录,兼容 App 未传参) |
+| `labelId` | 列表出参改为当日序号 `yyyyMMdd-n`;`labelEntityId` 为 `fl_label.Id` |
+
+### 请求示例(有数据的日期)
+
+```bash
+curl -X POST "http://192.168.1.4:19001/api/app/us-app-labeling/get-print-log-list" \
+ -H "Authorization: Bearer {token}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "locationId": "3a218397-8dda-a378-e024-ef89bcef8d24",
+ "skipCount": 1,
+ "maxResultCount": 20,
+ "printDateDay": "2026-06-01"
+ }'
+```
+
+或使用 `"printDate": "2026-06-01"`(等价)。
+
+### 权限说明
+
+非 admin / 非 Partner 角色时,仅返回 **`CreatedBy = 当前用户`** 的记录;若 Token 用户不是打印人,即使日期正确也会空列表。
+
+### 部署
+
+修改在 `FoodLabeling.Application`,需 **停服 → dotnet build → 重启** 后 Postman 才生效。
+
+### 涉及代码
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolvePrintLogFilterCalendarDay`、`ApplyPrintTaskCalendarDayFilter` |
+| `Dtos/UsAppLabeling/PrintLogGetListInputVo.cs` | `PrintDateDay` |
+| `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` |
+
+---
+
+## 十一、角色编辑 `PUT /api/app/rbac-role/{id}` — accessPermissions
+
+### 现象
+
+编辑角色传 `"accessPermissions": "[\"manage_labels\"]"` 后权限未生效或菜单绑定被清空。
+
+### 根因
+
+前端/Web 约定 `accessPermissions` 为 **JSON 数组字符串**(见 `rbacRoleService.ts` 的 `JSON.stringify(codes)`),后端 `ParseAccessPermissionCodes` 原先仅按逗号拆分,整段 `["manage_labels"]` 被当成一个非法 token,`NormalizeAccessPermissionCodes` 过滤后为空 → **RoleMenu 被清空**。
+
+### 修复
+
+`RbacAccessPermissionHelper.ParseAccessPermissionCodes`:若字符串以 `[` 开头,先 `JsonSerializer.Deserialize>`,再回退逗号分隔。
+
+### 合法 accessPermissions 值
+
+| 编码 | 说明 |
+|------|------|
+| `manage_labels` | 标签相关菜单(/labeling、/labels 等) |
+| `manage_people` | 账号管理 |
+| `edit_settings` | 菜单/多选项设置 |
+| `view_reports` | 报表 |
+| `manage_products` / `approve_batches` | 当前无独立菜单,不阻断其它权限 |
+
+### 请求示例
+
+```json
+{
+ "roleName": "Staff",
+ "roleCode": "Staff",
+ "remark": null,
+ "dataScope": 0,
+ "state": true,
+ "orderNum": 0,
+ "accessPermissions": "[\"manage_labels\"]"
+}
+```
+
+亦可传 `"accessPermissionCodes": ["manage_labels"]`(与 `accessPermissions` 合并去重)。
+
+### 涉及代码
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/RbacAccessPermissionHelper.cs` | JSON 数组解析 |
+| `Helpers/RoleAccessPermissionMenuMapping.cs` | UI 权限码 → Menu.Router |
+| `Services/RbacRoleAppService.cs` | `UpdateAsync` / `ApplyRoleMenuBindingsAsync` |
+
+---
+
+## 十二、Team Member — Company Admin 范围绑定
+
+### 规则
+
+角色为 **Company Admin**(库内常为 **Partner Admin**,或 RoleCode 含 `partner`)时:
+
+| 字段 | 是否必填 | 说明 |
+|------|----------|------|
+| `partnerId` / `partnerIds` | **是** | 指定适用 Company |
+| `regionIds` | **否** | 未传时后端自动绑定该公司下**全部 Region**(出参展示) |
+| `locationIds` / `assignedLocations` | **否** | 未传时后端自动绑定该公司下**全部门店** 并写入 `userlocation` |
+
+其它角色仍须 Company / Region / 门店至少一项能解析出门店。
+
+### 请求示例(Create / Update)
+
+```json
+{
+ "fullName": "Jane Doe",
+ "userName": "jane.doe",
+ "password": "123456",
+ "email": "jane@example.com",
+ "roleId": "{Partner Admin 角色 Guid}",
+ "partnerId": "{fl_partner.Id}",
+ "state": true
+}
+```
+
+无需传 `regionIds`、`locationIds`。
+
+### 列表出参
+
+`GET /api/app/team-member` 对 Company Admin 成员:
+
+- `roleName` 统一为 **Company Admin**(库内 Partner Admin 也会转换)
+- `regionIds` 展示该公司下**全部 Region**
+- `assignedLocations` 展示该公司下**全部门店**(无需前端再选手动 Region/Locations)
+
+### 编辑弹窗(Web)
+
+选 **Company Admin** 时隐藏 Region / Locations 字段,仅保留 **Company**;保存时只传 `partnerId`,后端自动展开绑定。
+
+### 涉及代码
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/TeamMemberRoleHelper.cs` | Company Admin 角色识别 |
+| `Helpers/LocationScopeBindingHelper.cs` | `ResolveGroupIdsFromPartnerIdsAsync` |
+| `Services/TeamMemberAppService.cs` | 保存/列表/详情 scope 逻辑 |
+| `PeopleView.tsx` | 前端 Company Admin 放宽 Region/Locations 必填 |
+
+---
+
+## 十三、Team Member 批量导入 / PDF 导出 — Region 列
+
+### 模板列(下载 `download-team-member-import-template`)
+
+| 列 | 必填 | 说明 |
+|----|------|------|
+| Name / Email / Role Name | 是 | 与单条创建一致 |
+| **Region** | **否** | 可留空;填 `fl_group.Id` 或 Region 名称(GroupName),多个用 `;` 分隔 |
+| **Assigned Location Ids** | **否*** | 与 Region **至少填一项**;见下 |
+
+\* Company Admin 在 Web 端可只选 Company;Excel 批量导入仍须 Region 或门店至少一项(或后续扩展 Company 列)。
+
+### Assigned Location Ids 填什么?
+
+**优先填 `location.LocationCode`(门店编码)**,例如 `LOC001`、`33333`。
+
+也支持:
+
+- **`location.Id`(Guid 字符串)**
+- **`编码 - 门店名`** 格式(取 ` -` 前一段作为编码或 Guid)
+
+模板示例 `LOC001;LOC002` 即为 **LocationCode**,不是 Guid。
+
+### 涉及代码
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/TeamMemberBatchExcelHelper.cs` | Region 列解析、`BuildImportTemplateWorkbook` |
+| `Services/TeamMemberAppService.cs` | 导入解析 Region/Location、PDF 增 Region 列 |
+
+---
+
+## 十四、Team Member 列表只返回 1 条(应 3 条)
+
+### 现象
+
+`GET /api/app/team-member?SkipCount=1&MaxResultCount=10` 在 Subway China 下应返回 **123 / Sandi / Tom** 共 3 人,实际 `totalCount=1`。
+
+### 根因
+
+1. **Guid 字符串大小写不一致**:`userlocation.UserId` / `LocationId` 与 `User.Id.ToString()` 比较时用字符串 `Contains`,大小写不同会漏关联,导致成员 scope 命中失败、`assignedLocations` 为空。
+2. **前端未传 `PartnerId`**:筛选 Company 时只靠客户端二次过滤,且 `locationIds` 为空时被误过滤。
+3. **列表出参缺 `locationIds`**:前端无法正确做门店 scope 匹配。
+
+### 修复
+
+| 改动 | 说明 |
+|------|------|
+| `TeamMemberListScopeHelper` | 统一 `NormalizeScopeKey` / `UserKey`,用 **Guid** 比较 |
+| `BuildFilteredUserQueryAsync` | 按 Guid 解析 scope 与 userlocation |
+| `MapUsersToOutputAsync` | assignedMap 用规范化 userKey;出参增加 **`locationIds`** |
+| 前端 `getTeamMembers` | 传 **`PartnerId` / `GroupId`** |
+| `memberMatchesLocationScope` | 支持 **partnerIds** 匹配 + locationId 小写归一 |
+
+### 验证(测试库 Subway China)
+
+| 用户 | 应出现在 Subway 范围列表 |
+|------|--------------------------|
+| 123 / Sandi / Tom | 是 |
+| Nancy Lang(GongCha) | 否 |
+| admin | 仅平台管理员无筛选时可见 |
+
+---
+
+## 十五、Region(Group)列表 Company Admin 只返回 1 条
+
+### 现象
+
+`GET /api/app/group?SkipCount=1&MaxResultCount=10&Sorting=CreationTime+desc` 以 **Company Admin(Subway China / 用户 123)** 登录时,`totalCount=1`,仅 **Subway Beijing**;测试库 Subway China 下应有 **Beijing / Chendu / Shanghai** 共 3 条。
+
+### 根因
+
+`PartnerScopeHelper.ResolveGroupScopeAsync` 对非管理员按 **userlocation 绑定的门店** 推导 Region:只匹配 `location.Partner + location.GroupName` 对应的 `fl_group`。用户 123 仅绑定了 Beijing 门店,因此 scope 只有 **Subway Beijing** 一条,未展开到公司下全部 Region。
+
+### 修复
+
+| 文件 | 说明 |
+|------|------|
+| `Helpers/LocationScopeBindingHelper.cs` | 新增 `ResolveBoundLocationIdsForUserAsync`、`ResolveDisplayRegionIdsForUserAsync`(与 Team Member 详情 `regionIds` 同规则) |
+| `Helpers/PartnerScopeHelper.cs` | `ResolveGroupScopeAsync` 改为调用上述方法;`ApplyGroupScope` 用 Guid 比较;`ResolvePartnerScopeAsync` 统一 Guid 读 `userlocation` |
+
+### 验证
+
+- 用户 **123**(Company Admin)→ `totalCount=3`,`items` 与编辑接口 `regionIds` 三条一致
+- 平台 **admin** → 全部 4 条
+- **SkipCount=1** 表示第一页(页码从 1 起),不是 offset
+
+---
+
+## 关联文档
+
+- App Preview labelId / Company:`项目相关文档/6-11代码优化.md`
+- 第一轮 scope 兼容:`项目相关文档/6-17代码优化.md`(第一节、第二节 get-print-log-list)
+- 三维 scope 业务规则:`项目相关文档/6-4代码优化.md`
+- 分页约定:`Helpers/PagedQueryConvention.cs`
diff --git a/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv b/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv
index 32ae262..86ba811 100644
--- a/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv
+++ b/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv
@@ -1,3 +1,3 @@
-Name,User Name (Login),Password,Email,Phone,Role Id,Role Name (仅说明勿删),Assigned Location Ids,Status
-John Doe,john.doe,ChangeMe123!,john@123.com,789654444,"(必填)从系统角色列表复制 Role Id","Staff","门店Guid1;门店Guid2",TRUE
+Name,User Name,Password,Email,Phone,Role Id,Role Name,Region,Assigned Location Ids,Status
+John Doe,john.doe,ChangeMe123!,john@example.com,789654444,,Staff,,LOC001;LOC002,TRUE
,,,,,,,,,
diff --git a/项目相关文档/批量导入导出接口说明.md b/项目相关文档/批量导入导出接口说明.md
index 83e9853..453c539 100644
--- a/项目相关文档/批量导入导出接口说明.md
+++ b/项目相关文档/批量导入导出接口说明.md
@@ -178,7 +178,7 @@
| 数据范围 | **全量**:符合筛选条件的全部成员;**不使用** `SkipCount` / `MaxResultCount` |
| 排序 | 有 `Sorting` 则按其排序,否则按创建时间降序 |
| 响应 | `Content-Type: application/pdf`,文件名示例 `team-members_yyyy-MM-dd_HH-mm-ss.pdf` |
-| PDF 列 | Name、Email、Phone、Role、Assigned Locations(多门店以分号拼接)、Status(Active/Inactive) |
+| PDF 列 | Name、Email、Phone、Role、**Region**、Assigned Locations(多门店以分号拼接)、Status(Active/Inactive) |
**说明**:PDF 中「Assigned Locations」展示该成员**全部**已分配门店(不受列表按门店筛选时「仅显示命中门店」的收缩影响),便于导出后审阅完整权限。
@@ -195,10 +195,14 @@
**表头识别(摘要)**
-- **必填列**:`Name`(或 FullName)、`Email`;**Role**;**Assigned Locations**(至少一条)。
-- **可选列**:`UserName` / `Login`(不填则登录账号用 Email)、`Password`(不填则用配置 **`TeamMemberImportDefaultPassword`**)、`Phone`、`Status`。
-- **Assigned Locations**:多个门店可用 **`;`**、`|`、换行、中文 **`,`** 分隔;支持 `33333 - Central Park Store`(取 **` -`** 前为门店编码或 Guid)。
-- **Role**:与系统 **`Role.RoleName`** 一致(忽略大小写与中间空格);未匹配则该行失败。
+- **必填列**:`Name`(或 FullName)、`Email`;**Role Name**(或 **Role Id** Guid);**Region** 与 **Assigned Location Ids** **至少填一项**(均可留空仅适用于 Web 已支持的 Company Admin + Company 场景)。
+- **可选列**:`User Name` / `Login`(不填则登录账号用 Email)、`Password`(不填则用配置 **`TeamMemberImportDefaultPassword`**)、`Phone`、`Status`、**`Region`**(可留空)。
+- **Region**:多个可用 **`;`**、`|`, 换行分隔;支持 **`fl_group.Id`(Guid)** 或 **Region 名称(GroupName)**;仅填 Region 时会绑定该区域下全部门店。
+- **Assigned Location Ids**:多个可用 **`;`**、`|`, 换行、中文 **`,`** 分隔;支持 **`location.LocationCode`(门店编码,推荐)** 或 **`location.Id`(Guid)**;亦支持 `LOC001 - Store Name`(取 **` -`** 前为编码或 Guid)。
+- **Role**:与系统 **`Role.RoleName`** 一致(忽略大小写与中间空格);或填 **Role Id**(Guid)。
+- 下载模板由服务端 **动态生成**(含 **Region** 列),不再依赖服务器目录中的静态 xlsx。
+
+**PDF 导出列**:Name、Email、Phone、Role、**Region**、Assigned Locations、Status。
内部对每行调用与单条创建相同的业务逻辑;**单行失败不影响其它行**。