From acf259d52f25691ed733e95dc0d8db0fbf0ba5b2 Mon Sep 17 00:00:00 2001
From: 李曜臣
Date: Fri, 24 Jul 2026 17:04:43 +0800
Subject: [PATCH] 2026-07-24
---
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs | 7 ++++++-
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs | 5 ++++-
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThWebPlatformLoginHelper.cs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs | 5 ++++-
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs | 13 ++++++++++---
泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json | 2 +-
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineInputVo.cs | 12 ++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineResultDto.cs | 31 +++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineInputVo.cs | 12 ++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineResultDto.cs | 31 +++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs | 12 ++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs | 31 +++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs | 12 ++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs | 31 +++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs | 13 ++++++++++---
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs | 3 ++-
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs | 11 ++++++++---
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs | 29 +++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs | 31 +++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs | 31 +++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs | 39 +++++++++++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs | 25 +++++++++++++++++++++----
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs | 21 +++++++++++++++------
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs | 47 ++++++++++++++++++++++++++++++++++++++---------
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs | 50 +++++++++++++++++++++++++++++++++++++++++++++++++-
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs | 41 +++++++++++++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs | 41 +++++++++++++++++++++++++++++++++++++++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json | 6 +++---
项目相关文档/2026-07-23泰额版平台与公司登录拉菜单用法.md | 402 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/2026-07-23泰额版当前登录账号菜单接口.md | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/2026-07-23美国版在线批量导入接口.md | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/2026-07-24代码优化.md | 328 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/th-platform-user-role-seed.sql | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/th-tenant-menu-seed.sql | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目相关文档/批量导入导出接口说明.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
40 files changed, 3004 insertions(+), 84 deletions(-)
create mode 100644 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThWebPlatformLoginHelper.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineInputVo.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineResultDto.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineInputVo.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineResultDto.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs
create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs
create mode 100644 项目相关文档/2026-07-23泰额版平台与公司登录拉菜单用法.md
create mode 100644 项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md
create mode 100644 项目相关文档/2026-07-23泰额版当前登录账号菜单接口.md
create mode 100644 项目相关文档/2026-07-23美国版在线批量导入接口.md
create mode 100644 项目相关文档/2026-07-24代码优化.md
create mode 100644 项目相关文档/th-platform-user-role-seed.sql
create mode 100644 项目相关文档/th-tenant-menu-seed.sql
diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs
index 703ddaa..e29bc69 100644
--- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs
@@ -40,7 +40,12 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService
///
public virtual async Task GetMyMenusAsync()
{
- TenantContextGuard.EnsureTenantResolved(CurrentTenant, "获取菜单权限");
+ // 平台 Token(JWT 无 TenantId)读主库;公司 Token 须已解析租户上下文
+ if (CurrentTenant.Id.HasValue)
+ {
+ TenantContextGuard.EnsureTenantResolved(CurrentTenant, "获取菜单权限");
+ }
+
if (!CurrentUser.Id.HasValue)
{
throw new UserFriendlyException("用户未登录");
diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs
index 518585f..8f4160f 100644
--- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs
@@ -1,10 +1,13 @@
namespace FoodLabeling.Th.Application.Contracts.Dtos.MultiTenancy;
///
-/// 登录页租户下拉项(不含租户 Id 与连接串)
+/// 登录页租户下拉项(含 Id 供 H5 传 tenantId;不含连接串)
///
public class ThTenantSelectDto
{
+ /// 租户 Id(登录 th-web-auth/login 时作为 tenantId)
+ public Guid Id { get; set; }
+
/// 租户名称
public string Name { get; set; } = string.Empty;
diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThWebPlatformLoginHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThWebPlatformLoginHelper.cs
new file mode 100644
index 0000000..8bd922a
--- /dev/null
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThWebPlatformLoginHelper.cs
@@ -0,0 +1,51 @@
+namespace FoodLabeling.Th.Application.MultiTenancy;
+
+///
+/// 泰额 H5 同一登录框:Default / 空租户走主库平台登录;具体公司 Guid 走租户业务库。
+///
+public static class ThWebPlatformLoginHelper
+{
+ /// 主库 Default 租户 Id(业务库可能指向 US,平台登录不得落入该库)
+ public static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
+
+ /// Default 租户显示名
+ public const string DefaultTenantName = "Default";
+
+ /// 平台登录成功时 th-web-auth 返回的 tenantName(无 TenantId Claim)
+ public const string PlatformTenantDisplayName = "Platform";
+
+ ///
+ /// 是否应优先尝试主库平台登录(与前端选 Default / 空 tenantId 一致)
+ ///
+ public static bool ShouldTryPlatformLogin(Guid? tenantId, string? tenantName)
+ {
+ if (!tenantId.HasValue || tenantId.Value == Guid.Empty)
+ {
+ return true;
+ }
+
+ if (tenantId.Value == DefaultTenantId)
+ {
+ return true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(tenantName)
+ && string.Equals(tenantName.Trim(), DefaultTenantName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// 平台账号须为邮箱(与 account/login 一致)
+ ///
+ public static bool IsPlausiblePlatformEmail(string userName)
+ {
+ var email = userName.Trim();
+ return email.Contains('@')
+ && !email.StartsWith('@')
+ && !email.EndsWith('@');
+ }
+}
diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs
index 4bcdb52..549da0a 100644
--- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs
@@ -83,12 +83,13 @@ public class ThMultiTenancyAppService : ApplicationService, IThMultiTenancyAppSe
/// 获取登录页可选租户列表(含默认管理员加密凭据)
///
///
- /// 供 Web 登录页租户下拉使用;匿名可访问。不返回租户 Id 与数据库连接串。
+ /// 供 Web 登录页租户下拉使用;匿名可访问。返回租户 Id 与名称(不含数据库连接串)。
///
/// 示例响应:
/// ```json
/// [
/// {
+ /// "id": "11111111-1111-1111-1111-111111111111",
/// "name": "Default",
/// "creationTime": "2026-01-01T08:00:00",
/// "loginAccount": "admin",
@@ -99,6 +100,7 @@ public class ThMultiTenancyAppService : ApplicationService, IThMultiTenancyAppSe
/// ```
///
/// 字段说明:
+ /// - id: 租户 Id,登录 th-web-auth/login 时传 tenantId;选 Default 且用平台邮箱则走主库平台登录
/// - name: 租户名称,登录时可传 tenantName
/// - creationTime: 租户创建时间
/// - loginAccount: 管理员登录账号,优先读凭据表,否则为 admin
@@ -126,6 +128,7 @@ public class ThMultiTenancyAppService : ApplicationService, IThMultiTenancyAppSe
var resolved = ResolveEncryptedCredential(credential);
return new ThTenantSelectDto
{
+ Id = x.Id,
Name = x.Name,
CreationTime = x.CreationTime,
LoginAccount = resolved.LoginAccount,
diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs
index 9bc0e54..4b9ef60 100644
--- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs
@@ -48,14 +48,15 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService
///
/// 校验租户后在租户独立库验证账号,签发含 TenantId 与 RBAC 权限的 JWT。
///
- /// 示例请求(tenant-select 不再返回 Id 时使用 tenantName):
+ /// 泰额 H5 与公司 Web 共用此接口。选 **Default** 或 tenantId 为空时,若邮箱账号存在于主库则走**平台登录**(JWT 无 TenantId);
+ /// 选具体公司 tenantId 时在该公司业务库校验。Default 业务库(如 US)仅在主库无该邮箱时作为回落。
+ ///
+ /// 示例请求(平台,选 Default):
/// ```json
/// {
- /// "tenantName": "Default",
- /// "userName": "admin",
- /// "password": "123456",
- /// "uuid": null,
- /// "code": null
+ /// "tenantId": "11111111-1111-1111-1111-111111111111",
+ /// "userName": "admin@example.com",
+ /// "password": "123456"
/// }
/// ```
///
@@ -82,6 +83,21 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService
ValidationImageCaptcha(input.Uuid, input.Code);
+ // H5 登录页选 Default / 空 tenantId:优先主库平台账号(admin@example.com),避免误入 Default 业务库(如 US)
+ if (ThWebPlatformLoginHelper.ShouldTryPlatformLogin(input.TenantId, input.TenantName))
+ {
+ var platformLogin = await TryPlatformLoginAsync(input);
+ if (platformLogin != null)
+ {
+ return platformLogin;
+ }
+
+ if (!input.TenantId.HasValue || input.TenantId.Value == Guid.Empty)
+ {
+ input.TenantId = ThWebPlatformLoginHelper.DefaultTenantId;
+ }
+ }
+
TenantConfiguration tenantConfig;
try
{
@@ -140,6 +156,43 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService
}
}
+ ///
+ /// 主库平台登录:CurrentTenant 为空签发无 TenantId 的 JWT,供 Vue3Router/vben5 使用。
+ /// 主库无对应邮箱用户时返回 null,由调用方回落 Default/公司租户库登录。
+ ///
+ private async Task TryPlatformLoginAsync(ThWebLoginInputVo input)
+ {
+ if (!ThWebPlatformLoginHelper.IsPlausiblePlatformEmail(input.UserName))
+ {
+ return null;
+ }
+
+ using (CurrentTenant.Change(null))
+ {
+ var user = await FindActiveUserByEmailAsync(input.UserName.Trim());
+ if (user == null)
+ {
+ return null;
+ }
+
+ if (!user.JudgePassword(input.Password))
+ {
+ throw new UserFriendlyException("登录失败:账号或密码错误");
+ }
+
+ var accessToken = await _accountManager.GetTokenByUserIdAsync(user.Id);
+ var refreshToken = _accountManager.CreateRefreshToken(user.Id);
+
+ return new ThWebLoginOutputDto
+ {
+ Token = accessToken,
+ RefreshToken = refreshToken,
+ TenantId = Guid.Empty,
+ TenantName = ThWebPlatformLoginHelper.PlatformTenantDisplayName
+ };
+ }
+ }
+
private void ValidationImageCaptcha(string? uuid, string? code)
{
if (!_rbacOptions.EnableCaptcha)
diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs
index edad015..37e7d2b 100644
--- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs
+++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs
@@ -514,9 +514,16 @@ namespace Yi.Framework.Rbac.Application.Services
}
else if ( routerType == "vben5")
{
- //将后端菜单转换成前端路由,组件级别需要过滤
- output =
- ObjectMapper.Map, List>(menus.Where(x=>x.MenuSource==MenuSourceEnum.Vben5).ToList()).Vben5RouterBuild();
+ // 泰额 H5 拉 vben5 路由;主库平台菜单多为 MenuSource=Ruoyi,空则回退 Ruoyi 再 Vben5 构建
+ var vben5Menus = menus.Where(x => x.MenuSource == MenuSourceEnum.Vben5).ToList();
+ if (vben5Menus.Count == 0)
+ {
+ vben5Menus = menus.Where(x => x.MenuSource == MenuSourceEnum.Ruoyi).ToList();
+ }
+
+ output = ObjectMapper
+ .Map, List>(vben5Menus)
+ .Vben5RouterBuild();
}
return output;
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 ac3f716..f041cd5 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://192.168.31.248:19002",
+ "SelfUrl": "http://192.168.31.88:19002",
"CorsOrigins": "http://localhost:19002;http://localhost:18000;http://localhost:5666;http://localhost:5173;http://localhost:5174;http://localhost:3000;http://127.0.0.1:19002"
},
//配置
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineInputVo.cs
new file mode 100644
index 0000000..1166417
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineInputVo.cs
@@ -0,0 +1,12 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Group;
+
+///
+/// Region(Group)JSON 在线批量导入请求体
+///
+public class GroupBatchImportOnlineInputVo
+{
+ ///
+ /// 待导入行,每元素与单条 POST /api/app/group 的 一致
+ ///
+ public List Items { get; set; } = new();
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineResultDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineResultDto.cs
new file mode 100644
index 0000000..19b1b78
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineResultDto.cs
@@ -0,0 +1,31 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Group;
+
+///
+/// Region(Group)JSON 在线批量导入结果
+///
+public class GroupBatchImportOnlineResultDto
+{
+ public int SuccessCount { get; set; }
+
+ public int FailCount { get; set; }
+
+ public List Errors { get; set; } = new();
+}
+
+///
+/// Region(Group)JSON 在线批量导入单条失败信息
+///
+public class GroupBatchImportOnlineErrorDto
+{
+ ///
+ /// 在请求 items 数组中的序号(从 0 开始)
+ ///
+ public int Index { get; set; }
+
+ ///
+ /// Region 名称(groupName),便于定位失败行
+ ///
+ public string? GroupName { get; set; }
+
+ public string Message { get; set; } = string.Empty;
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineInputVo.cs
new file mode 100644
index 0000000..46ae97d
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineInputVo.cs
@@ -0,0 +1,12 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Location;
+
+///
+/// 门店 JSON 在线批量导入请求体
+///
+public class LocationBatchImportOnlineInputVo
+{
+ ///
+ /// 待导入行,每元素与单条 POST /api/app/location 的 一致
+ ///
+ public List Items { get; set; } = new();
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineResultDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineResultDto.cs
new file mode 100644
index 0000000..ba18c41
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineResultDto.cs
@@ -0,0 +1,31 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Location;
+
+///
+/// 门店 JSON 在线批量导入结果
+///
+public class LocationBatchImportOnlineResultDto
+{
+ public int SuccessCount { get; set; }
+
+ public int FailCount { get; set; }
+
+ public List Errors { get; set; } = new();
+}
+
+///
+/// 门店 JSON 在线批量导入单条失败信息
+///
+public class LocationBatchImportOnlineErrorDto
+{
+ ///
+ /// 在请求 items 数组中的序号(从 0 开始)
+ ///
+ public int Index { get; set; }
+
+ ///
+ /// 门店编码(locationCode),便于定位失败行
+ ///
+ public string? LocationCode { get; set; }
+
+ public string Message { get; set; } = string.Empty;
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs
new file mode 100644
index 0000000..9707f4d
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs
@@ -0,0 +1,12 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+///
+/// 产品 JSON 在线批量导入请求体
+///
+public class ProductBatchImportOnlineInputVo
+{
+ ///
+ /// 待导入行,每元素与单条 POST /api/app/product 的 一致
+ ///
+ public List Items { get; set; } = new();
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs
new file mode 100644
index 0000000..acea7ed
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs
@@ -0,0 +1,31 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+///
+/// 产品 JSON 在线批量导入结果
+///
+public class ProductBatchImportOnlineResultDto
+{
+ public int SuccessCount { get; set; }
+
+ public int FailCount { get; set; }
+
+ public List Errors { get; set; } = new();
+}
+
+///
+/// 产品 JSON 在线批量导入单条失败信息
+///
+public class ProductBatchImportOnlineErrorDto
+{
+ ///
+ /// 在请求 items 数组中的序号(从 0 开始)
+ ///
+ public int Index { get; set; }
+
+ ///
+ /// 产品名称(productName),便于定位失败行
+ ///
+ public string? ProductName { get; set; }
+
+ public string Message { get; set; } = string.Empty;
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs
new file mode 100644
index 0000000..8ea1fbe
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs
@@ -0,0 +1,12 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+///
+/// 成员 JSON 在线批量导入请求体
+///
+public class TeamMemberBatchImportOnlineInputVo
+{
+ ///
+ /// 待导入行,每元素与单条 POST /api/app/team-member 的 一致
+ ///
+ public List Items { get; set; } = new();
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs
new file mode 100644
index 0000000..745beab
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs
@@ -0,0 +1,31 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+///
+/// 成员 JSON 在线批量导入结果
+///
+public class TeamMemberBatchImportOnlineResultDto
+{
+ public int SuccessCount { get; set; }
+
+ public int FailCount { get; set; }
+
+ public List Errors { get; set; } = new();
+}
+
+///
+/// 成员 JSON 在线批量导入单条失败信息
+///
+public class TeamMemberBatchImportOnlineErrorDto
+{
+ ///
+ /// 在请求 items 数组中的序号(从 0 开始)
+ ///
+ public int Index { get; set; }
+
+ ///
+ /// 登录账号(userName),便于定位失败行
+ ///
+ public string? UserName { get; set; }
+
+ public string Message { get; set; } = string.Empty;
+}
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 646bc84..09f316b 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,20 +28,27 @@ public class TeamMemberCreateInputVo
public List? PartnerIds { get; set; }
///
- /// 适用 Region 多选(fl_group.Id);Company Admin 仅传 Company 时可省略,后端自动绑定该公司下全部 Region 与门店
+ /// 适用 Region 多选(fl_group.Id);可含 ALL 哨兵(大小写不敏感,与具体 Guid 同传时以 ALL 为准)。
+ /// Company Admin 仅传 Company 时可省略;locationIds 为空且含 ALL 时展开该公司全部 Region 下门店。
///
public List? RegionIds { get; set; }
///
- /// 适用 Region 多选(与 相同)
+ /// 适用 Region 多选(与 相同);可含 ALL。
///
public List? GroupIds { get; set; }
///
- /// 适用门店多选(location.Id);Company Admin 仅传 Company 时可省略,与 Region 合并后写入 userlocation
+ /// 适用门店多选(location.Id);可含 ALL 哨兵,按 / 展开该公司全部门店落库。
+ /// Company Admin 仅传 Company 时可省略。
///
public List? LocationIds { get; set; }
+ ///
+ /// 适用门店多选别名(与 合并解析);可含 ALL 或门店 Guid。
+ ///
+ public List? Locations { get; set; }
+
public bool State { get; set; } = true;
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs
index bf245f9..9df7631 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs
@@ -19,12 +19,13 @@ public class TeamMemberGetOutputDto
/// 适用 Company Id(多选,由绑定门店反推)
public List PartnerIds { get; set; } = new();
- /// 适用 Region Id(多选,fl_group.Id)
+ /// 适用 Region Id(多选,fl_group.Id;覆盖该公司全部 Region 时回显 ["ALL"])
public List RegionIds { get; set; } = new();
/// 与 相同
public List GroupIds { get; set; } = new();
+ /// 绑定门店 Id;覆盖该公司全部门店时回显 ["ALL"](库中仍存展开 Guid)
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 163b529..6de4648 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,20 +28,25 @@ public class TeamMemberUpdateInputVo
public List? PartnerIds { get; set; }
///
- /// 适用 Region 多选(fl_group.Id);Company Admin 仅传 Company 时可省略
+ /// 适用 Region 多选(fl_group.Id);可含 ALL 哨兵(大小写不敏感,与具体 Guid 同传时以 ALL 为准)。
///
public List? RegionIds { get; set; }
///
- /// 适用 Region 多选(与 相同)
+ /// 适用 Region 多选(与 相同);可含 ALL。
///
public List? GroupIds { get; set; }
///
- /// 适用门店多选(location.Id);Company Admin 仅传 Company 时可省略
+ /// 适用门店多选(location.Id);可含 ALL 哨兵,按 Company 展开全部门店落库。
///
public List? LocationIds { get; set; }
+ ///
+ /// 适用门店多选别名(与 合并解析);可含 ALL 或门店 Guid。
+ ///
+ public List? Locations { get; set; }
+
public bool State { get; set; } = true;
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs
index ee24ac5..80258f9 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs
@@ -56,4 +56,33 @@ public interface IGroupAppService : IApplicationService
/// Keyword、PartnerId、State、Sorting;分页字段忽略
/// application/pdf
Task ExportPdfAsync(GroupGetListInputVo input);
+
+ ///
+ /// JSON 在线批量导入 Region(逐行调用 ,部分成功)
+ ///
+ ///
+ /// 请求体为 JSON,每行字段与单条新增 一致。
+ ///
+ /// 示例请求:
+ /// ```json
+ /// {
+ /// "items": [
+ /// {
+ /// "groupName": "NC Region",
+ /// "partnerId": "PARTNER_ID",
+ /// "state": true
+ /// }
+ /// ]
+ /// }
+ /// ```
+ ///
+ /// 参数说明:
+ /// - items: 待导入行数组;index 从 0 起;单次最多 MaxImportRows 条(默认 5000)
+ ///
+ /// 批量导入请求体
+ /// 成功数、失败数及失败明细(index、groupName、message)
+ /// 全部或部分行处理完成,见返回体中的计数与 errors
+ /// 整单校验失败(如 items 为空、超过单次条数上限)
+ /// 服务器错误
+ Task BatchImportOnlineAsync(GroupBatchImportOnlineInputVo input);
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs
index 0933892..feb7002 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs
@@ -90,5 +90,36 @@ public interface ILocationAppService : IApplicationService
/// 整单校验失败(如超过单次条数上限、items 为空)
/// 服务器错误
Task UpdateLocationsBulkAsync(LocationBulkUpdateInputVo input);
+
+ ///
+ /// JSON 在线批量导入门店(逐行调用 ,部分成功)
+ ///
+ ///
+ /// 请求体为 JSON,每行字段与单条新增 一致。
+ ///
+ /// 示例请求:
+ /// ```json
+ /// {
+ /// "items": [
+ /// {
+ /// "locationCode": "LOC001",
+ /// "locationName": "UNCC store",
+ /// "partner": "MedVantage Cafe Group",
+ /// "groupName": "NC Region",
+ /// "state": true
+ /// }
+ /// ]
+ /// }
+ /// ```
+ ///
+ /// 参数说明:
+ /// - items: 待导入行数组;index 从 0 起;单次最多 MaxImportRows 条(默认 5000)
+ ///
+ /// 批量导入请求体
+ /// 成功数、失败数及失败明细(index、locationCode、message)
+ /// 全部或部分行处理完成,见返回体中的计数与 errors
+ /// 整单校验失败(如 items 为空、超过单次条数上限)
+ /// 服务器错误
+ Task BatchImportOnlineAsync(LocationBatchImportOnlineInputVo input);
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
index e40d991..57f8849 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
@@ -74,5 +74,36 @@ public interface IProductAppService : IApplicationService
/// 批量编辑产品(JSON 一次提交多行,与单条 PUT 字段一致)
///
Task UpdateProductsBulkAsync(ProductBulkUpdateInputVo input);
+
+ ///
+ /// JSON 在线批量导入产品(逐行调用 ,部分成功)
+ ///
+ ///
+ /// 请求体为 JSON,每行字段与单条新增 一致。
+ ///
+ /// 示例请求:
+ /// ```json
+ /// {
+ /// "items": [
+ /// {
+ /// "productName": "Tuna & Bacon Sub",
+ /// "categoryId": "CATEGORY_ID",
+ /// "productCode": "40001",
+ /// "state": true,
+ /// "locationIds": ["LOCATION_GUID"]
+ /// }
+ /// ]
+ /// }
+ /// ```
+ ///
+ /// 参数说明:
+ /// - items: 待导入行数组;index 从 0 起;单次最多 MaxImportRows 条(默认 5000)
+ ///
+ /// 批量导入请求体
+ /// 成功数、失败数及失败明细(index、productName、message)
+ /// 全部或部分行处理完成,见返回体中的计数与 errors
+ /// 整单校验失败(如 items 为空、超过单次条数上限)
+ /// 服务器错误
+ Task BatchImportOnlineAsync(ProductBatchImportOnlineInputVo input);
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
index 1c8c36d..63564d8 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
@@ -14,8 +14,14 @@ public interface ITeamMemberAppService
Task GetAsync(Guid id);
+ ///
+ /// 新增成员(POST /api/app/team-member)。locationIds / regionIds / groupIds / locations 可传 ALL 哨兵。
+ ///
Task CreateAsync(TeamMemberCreateInputVo input);
+ ///
+ /// 更新成员(PUT /api/app/team-member/{id})。范围传参规则与 相同。
+ ///
Task UpdateAsync(Guid id, TeamMemberUpdateInputVo input);
Task DeleteAsync(Guid id);
@@ -39,4 +45,37 @@ public interface ITeamMemberAppService
/// 批量编辑成员(JSON 一次提交多行)
///
Task UpdateTeamMembersBulkAsync(TeamMemberBulkUpdateInputVo input);
+
+ ///
+ /// JSON 在线批量导入成员(逐行调用 ,部分成功)
+ ///
+ ///
+ /// 请求体为 JSON,每行字段与单条新增 一致;
+ /// password 为空时使用配置 TeamMemberImportDefaultPassword。
+ ///
+ /// 示例请求:
+ /// ```json
+ /// {
+ /// "items": [
+ /// {
+ /// "fullName": "John Doe",
+ /// "userName": "john@example.com",
+ /// "email": "john@example.com",
+ /// "roleId": "ROLE_GUID",
+ /// "locationIds": ["LOCATION_GUID"],
+ /// "state": true
+ /// }
+ /// ]
+ /// }
+ /// ```
+ ///
+ /// 参数说明:
+ /// - items: 待导入行数组;index 从 0 起;单次最多 MaxImportRows 条(默认 5000)
+ ///
+ /// 批量导入请求体
+ /// 成功数、失败数及失败明细(index、userName、message)
+ /// 全部或部分行处理完成,见返回体中的计数与 errors
+ /// 整单校验失败(如 items 为空、超过单次条数上限)
+ /// 服务器错误
+ Task BatchImportOnlineAsync(TeamMemberBatchImportOnlineInputVo input);
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs
index 46b1dee..2bd7875 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs
@@ -23,22 +23,36 @@ public static class AllScopeBindingHelper
return selected.Count == 0;
}
- if (selected.Count != universe.Count)
+ var universeSet = new HashSet(
+ universe.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()),
+ StringComparer.OrdinalIgnoreCase);
+ var selectedSet = new HashSet(
+ selected.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()),
+ StringComparer.OrdinalIgnoreCase);
+
+ if (selectedSet.Count != universeSet.Count)
{
return false;
}
- var set = new HashSet(universe, StringComparer.Ordinal);
- return selected.All(id => set.Contains(id));
+ return selectedSet.SetEquals(universeSet);
}
- /// 解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。
+ ///
+ /// 解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。
+ /// 入参显式 时保留 SPECIFIED 并落库全部 Id,不因「当前上下文全选」折叠为 ALL。
+ ///
public static string ResolveDimensionType(
string? declaredType,
IReadOnlyList ids,
bool hasArrayInPayload,
bool isFullSelection)
{
+ if (IsDeclaredSpecified(declaredType))
+ {
+ return ScopeSpecified;
+ }
+
if (isFullSelection || (ids.Count == 0 && IsDeclaredAll(declaredType)))
{
return ScopeAll;
@@ -61,6 +75,9 @@ public static class AllScopeBindingHelper
public static bool IsDeclaredAll(string? type) =>
string.Equals((type ?? ScopeAll).Trim(), ScopeAll, StringComparison.OrdinalIgnoreCase);
+ public static bool IsDeclaredSpecified(string? type) =>
+ string.Equals((type ?? string.Empty).Trim(), ScopeSpecified, StringComparison.OrdinalIgnoreCase);
+
/// 全部 Company(fl_partner.Id)。
public static async Task> ResolveAllPartnerIdsAsync(ISqlSugarClient db)
{
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 674a63b..a40d117 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
@@ -63,17 +63,16 @@ public static class LabelTemplateScopeHelper
ISqlSugarClient db,
LabelTemplateCreateInputVo input)
{
- var partnerIds = NormalizePartnerIds(input);
var regionIds = NormalizeRegionIds(input);
var locationIds = MergeExplicitLocationIds(input);
var (partnerType, partnerIdsForSave) = await AllScopeBindingHelper.NormalizePartnerScopeAsync(
db,
input.AppliedPartnerType,
- partnerIds,
- null,
+ input.PartnerIds,
+ input.CompanyIds,
input.PartnerIds is not null || input.CompanyIds is not null);
- partnerIds = partnerIdsForSave;
+ var partnerIds = partnerIdsForSave;
var partnerContext = string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
? partnerIds
@@ -300,6 +299,8 @@ public static class LabelTemplateScopeHelper
.Where(x => templateIds.Contains(x.TemplateId))
.ToListAsync();
+ var storedScopeTypes = await LabelTemplateScopeSchemaHelper.GetAppliedScopeTypesMapAsync(db, templateIds);
+
var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList();
var regionIdSet = regionLinks.Select(x => x.GroupId).Distinct(StringComparer.Ordinal).ToList();
var locationIdSet = locationLinks.Select(x => x.LocationId).Distinct(StringComparer.Ordinal).ToList();
@@ -360,9 +361,16 @@ public static class LabelTemplateScopeHelper
var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll;
var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll;
+ if (storedScopeTypes.TryGetValue(template.Id, out var storedTypes))
+ {
+ partnerType = NormalizeScopeType(storedTypes.PartnerType, partnerType);
+ regionType = NormalizeScopeType(storedTypes.RegionType, regionType);
+ }
+
if (hasExtendedScope
&& pIds.Count == 0
- && lIds.Count > 0)
+ && lIds.Count > 0
+ && !string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))
{
pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds);
if (pIds.Count > 0)
@@ -373,7 +381,8 @@ public static class LabelTemplateScopeHelper
if (hasExtendedScope
&& rIds.Count == 0
- && lIds.Count > 0)
+ && lIds.Count > 0
+ && !string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))
{
rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds);
if (rIds.Count > 0)
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
index 2285075..56c211f 100644
--- 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
@@ -315,6 +315,77 @@ public static class LabelTemplateScopeSchemaHelper
return map.TryGetValue(templateId.Trim(), out var text) ? text : null;
}
+ /// 批量读取模板 Company/Region 维度 ALL/SPECIFIED;列不存在时返回空字典。
+ public static async Task> GetAppliedScopeTypesMapAsync(
+ ISqlSugarClient db,
+ IReadOnlyList templateIds)
+ {
+ var result = new Dictionary(StringComparer.Ordinal);
+ if (templateIds.Count == 0)
+ {
+ return result;
+ }
+
+ var status = await GetStatusAsync(db);
+ if (!status.HasAppliedPartnerTypeColumn && !status.HasAppliedRegionTypeColumn)
+ {
+ return result;
+ }
+
+ var ids = templateIds.Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct(StringComparer.Ordinal)
+ .ToArray();
+ if (ids.Length == 0)
+ {
+ return result;
+ }
+
+ var rows = await db.Ado.SqlQueryAsync(
+ """
+ SELECT Id, AppliedPartnerType, AppliedRegionType
+ FROM fl_label_template
+ WHERE Id IN (@ids)
+ """,
+ new { ids });
+
+ foreach (var row in rows)
+ {
+ if (string.IsNullOrWhiteSpace(row.Id))
+ {
+ continue;
+ }
+
+ var partnerType = status.HasAppliedPartnerTypeColumn
+ ? NormalizeScopeTypeValue(row.AppliedPartnerType)
+ : ScopeAll;
+ var regionType = status.HasAppliedRegionTypeColumn
+ ? NormalizeScopeTypeValue(row.AppliedRegionType)
+ : ScopeAll;
+ result[row.Id.Trim()] = (partnerType, regionType);
+ }
+
+ return result;
+ }
+
+ private static string NormalizeScopeTypeValue(string? type)
+ {
+ var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant();
+ return normalized == ScopeSpecified ? ScopeSpecified : ScopeAll;
+ }
+
+ private const string ScopeAll = "ALL";
+ private const string ScopeSpecified = "SPECIFIED";
+
+ private sealed class AppliedScopeTypesRow
+ {
+ public string Id { get; init; } = string.Empty;
+
+ public string? AppliedPartnerType { get; init; }
+
+ public string? AppliedRegionType { get; init; }
+ }
+
/// 已迁移 Contents 列时写入(未迁移则 no-op)。
public static async Task SetContentsAsync(ISqlSugarClient db, string templateId, string? contents)
{
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 7c8d213..4c47ffb 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
@@ -321,6 +321,94 @@ public static class LocationScopeBindingHelper
}
///
+ /// 多选 Id 是否含 ALL 哨兵(大小写不敏感)。与具体 Guid 同传时以 ALL 为准(全选)。
+ ///
+ public static bool IsAllScopeSentinel(string? value) =>
+ string.Equals(value?.Trim(), AllScopeBindingHelper.ScopeAll, StringComparison.OrdinalIgnoreCase);
+
+ /// Id 列表是否含 ALL 哨兵。
+ public static bool ContainsAllScopeSentinel(IReadOnlyList? ids) =>
+ ids?.Any(IsAllScopeSentinel) == true;
+
+ /// 去掉 ALL 哨兵,仅保留具体 Id。
+ public static List FilterConcreteScopeIds(IReadOnlyList? ids) =>
+ NormalizeIds(ids).Where(x => !IsAllScopeSentinel(x)).ToList();
+
+ ///
+ /// Team Member 门店范围落库(不做 partner+region+location 并集)。
+ /// 优先级:
+ /// 1) locationIds 含 ALL → 公司全部门店;
+ /// 2) 有具体 locationIds,且 regionIds 为空或为 ALL → 只绑这些门店(UI 在 Region=ALL 下再收窄门店);
+ /// 3) regionIds 含 ALL → 公司全部 Region 下门店;
+ /// 4) 具体 regionIds → 按 Region 展开(忽略同传 locationIds,保证多区域能落库);
+ /// 5) 仅 → 公司全部门店。
+ ///
+ public static async Task> ResolveTeamMemberLocationIdsForSaveAsync(
+ ISqlSugarClient db,
+ IReadOnlyList? partnerIds,
+ IReadOnlyList? regionIds,
+ IReadOnlyList? locationIds)
+ {
+ var normalizedPartners = NormalizeIds(partnerIds);
+ var locationHasAll = ContainsAllScopeSentinel(locationIds);
+ var concreteLocations = FilterConcreteScopeIds(locationIds);
+ var regionHasAll = ContainsAllScopeSentinel(regionIds);
+ var concreteRegions = FilterConcreteScopeIds(regionIds);
+
+ // 1. locationIds 含 ALL → 按 partner 展开全部门店
+ if (locationHasAll)
+ {
+ if (normalizedPartners.Count == 0)
+ {
+ throw new UserFriendlyException("选择全部门店时需指定 Company(partnerId / partnerIds)");
+ }
+
+ return await ResolveLocationIdsFromPartnerIdsAsync(db, normalizedPartners);
+ }
+
+ // 2. 具体门店 +(无区域 / 区域为 ALL)→ 只绑这些门店,避免 regionIds=ALL 盖掉单店
+ if (concreteLocations.Count > 0 && (regionHasAll || concreteRegions.Count == 0))
+ {
+ return concreteLocations;
+ }
+
+ // 3. regionIds 含 ALL(且无具体门店)→ 按 partner 展开全部 Region 下门店
+ if (regionHasAll)
+ {
+ if (normalizedPartners.Count == 0)
+ {
+ throw new UserFriendlyException("选择全部 Region 时需指定 Company(partnerId / partnerIds)");
+ }
+
+ return await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, normalizedPartners, null);
+ }
+
+ // 4. 具体 regionIds → 按 Region 展开门店(忽略同传 locationIds)
+ if (concreteRegions.Count > 0)
+ {
+ return await MergeToLocationIdsAsync(
+ db,
+ (IReadOnlyList?)null,
+ concreteRegions,
+ null);
+ }
+
+ // 5. 仅有具体 locationIds → 只绑这些门店
+ if (concreteLocations.Count > 0)
+ {
+ return concreteLocations;
+ }
+
+ // 6. 仅有 partner → 绑该公司全部门店
+ if (normalizedPartners.Count > 0)
+ {
+ return await MergeToLocationIdsAsync(db, normalizedPartners, null, null);
+ }
+
+ return new List();
+ }
+
+ ///
/// 标签类型/分类/多选项门店范围落库:仅按 Region/Location 合并,不因 Company(partner)展开并集。
///
public static async Task> ResolveEntityLocationIdsForSaveAsync(
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs
index cec6458..35efd90 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs
@@ -40,7 +40,7 @@ public static class TeamMemberScopeDisplayHelper
return assigned.ToList();
}
- if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0)
+ if (partnerIds.Count > 0)
{
var allPartnerLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(
db, partnerIds);
@@ -51,14 +51,6 @@ public static class TeamMemberScopeDisplayHelper
}
}
- var universeLocationIds = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
- db, partnerIds, regionIds, null);
- if (universeLocationIds.Count > 0 &&
- AllScopeBindingHelper.IsFullIdSelection(assignedIds, universeLocationIds))
- {
- return AllLocationDisplay;
- }
-
return assigned.ToList();
}
@@ -92,6 +84,43 @@ public static class TeamMemberScopeDisplayHelper
: id));
}
+ ///
+ /// 编辑回显:库中存展开后的 Guid;若绑定已覆盖 Company 下全部 Region/门店,则将对应 Id 列表折叠为 ["ALL"],供前端勾选 ALL。
+ ///
+ public static async Task<(List RegionIds, List LocationIds, List Assigned)>
+ CollapseScopeIdsToAllSentinelForEditAsync(
+ ISqlSugarClient db,
+ IReadOnlyList partnerIds,
+ IReadOnlyList regionIds,
+ IReadOnlyList locationIds,
+ IReadOnlyList assigned)
+ {
+ var partners = LocationScopeBindingHelper.NormalizeIds(partnerIds);
+ var regions = LocationScopeBindingHelper.NormalizeIds(regionIds);
+ var locations = LocationScopeBindingHelper.NormalizeIds(locationIds);
+ var assignedList = assigned?.ToList() ?? new List();
+
+ if (partners.Count == 0)
+ {
+ return (regions, locations, assignedList);
+ }
+
+ var allRegionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(db, partners);
+ if (allRegionIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(regions, allRegionIds))
+ {
+ regions = new List { AllScopeBindingHelper.ScopeAll };
+ }
+
+ var allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners);
+ if (allLocationIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(locations, allLocationIds))
+ {
+ locations = new List { AllScopeBindingHelper.ScopeAll };
+ assignedList = AllLocationDisplay;
+ }
+
+ return (regions, locations, assignedList);
+ }
+
public static string FormatLocationTextForList(IReadOnlyList assigned)
{
if (assigned.Count == 0)
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
index 8d05650..9d08cc9 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
@@ -2,8 +2,10 @@ using FoodLabeling.Application.Helpers;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.Dtos.Group;
using FoodLabeling.Application.Contracts.IServices;
+using FoodLabeling.Application.Options;
using FoodLabeling.Application.Services.DbModels;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
@@ -25,11 +27,16 @@ public class GroupAppService : ApplicationService, IGroupAppService
private readonly ISqlSugarDbContext _dbContext;
private readonly IGuidGenerator _guidGenerator;
+ private readonly IOptionsSnapshot _batchImportOptions;
- public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
+ public GroupAppService(
+ ISqlSugarDbContext dbContext,
+ IGuidGenerator guidGenerator,
+ IOptionsSnapshot batchImportOptions)
{
_dbContext = dbContext;
_guidGenerator = guidGenerator;
+ _batchImportOptions = batchImportOptions;
}
///
@@ -264,6 +271,47 @@ public class GroupAppService : ApplicationService, IGroupAppService
return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName };
}
+ ///
+ [HttpPost("group/batch-import-online")]
+ public async Task BatchImportOnlineAsync(
+ [FromBody] GroupBatchImportOnlineInputVo input)
+ {
+ if (input?.Items is null || input.Items.Count == 0)
+ {
+ throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)");
+ }
+
+ var opt = _batchImportOptions.Value;
+ var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows;
+ if (input.Items.Count > maxRows)
+ {
+ throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交");
+ }
+
+ var result = new GroupBatchImportOnlineResultDto();
+ for (var index = 0; index < input.Items.Count; index++)
+ {
+ var vo = input.Items[index];
+ try
+ {
+ await CreateAsync(vo);
+ result.SuccessCount++;
+ }
+ catch (UserFriendlyException ex)
+ {
+ result.FailCount++;
+ result.Errors.Add(new GroupBatchImportOnlineErrorDto
+ {
+ Index = index,
+ GroupName = vo.GroupName,
+ Message = ex.Message
+ });
+ }
+ }
+
+ return result;
+ }
+
private async Task> BuildGroupJoinedQueryAsync(
GroupGetListInputVo input)
{
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs
index 76542b9..a8fe938 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs
@@ -303,6 +303,47 @@ public class LocationAppService : ApplicationService, ILocationAppService
}
///
+ [HttpPost("location/batch-import-online")]
+ public async Task BatchImportOnlineAsync(
+ [FromBody] LocationBatchImportOnlineInputVo input)
+ {
+ if (input?.Items is null || input.Items.Count == 0)
+ {
+ throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)");
+ }
+
+ var opt = _batchImportOptions.Value;
+ var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows;
+ if (input.Items.Count > maxRows)
+ {
+ throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交");
+ }
+
+ var result = new LocationBatchImportOnlineResultDto();
+ for (var index = 0; index < input.Items.Count; index++)
+ {
+ var vo = input.Items[index];
+ try
+ {
+ await CreateAsync(vo);
+ result.SuccessCount++;
+ }
+ catch (UserFriendlyException ex)
+ {
+ result.FailCount++;
+ result.Errors.Add(new LocationBatchImportOnlineErrorDto
+ {
+ Index = index,
+ LocationCode = vo.LocationCode,
+ Message = ex.Message
+ });
+ }
+ }
+
+ return result;
+ }
+
+ ///
public async Task UpdateLocationsBulkAsync([FromBody] LocationBulkUpdateInputVo input)
{
if (input?.Items is null || input.Items.Count == 0)
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
index 9a11e6a..a16f90f 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
@@ -477,6 +477,47 @@ public class ProductAppService : ApplicationService, IProductAppService
}
///
+ [HttpPost("product/batch-import-online")]
+ public async Task BatchImportOnlineAsync(
+ [FromBody] ProductBatchImportOnlineInputVo input)
+ {
+ if (input?.Items is null || input.Items.Count == 0)
+ {
+ throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)");
+ }
+
+ var opt = _batchImportOptions.Value;
+ var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows;
+ if (input.Items.Count > maxRows)
+ {
+ throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交");
+ }
+
+ var result = new ProductBatchImportOnlineResultDto();
+ for (var index = 0; index < input.Items.Count; index++)
+ {
+ var vo = input.Items[index];
+ try
+ {
+ await CreateAsync(vo);
+ result.SuccessCount++;
+ }
+ catch (UserFriendlyException ex)
+ {
+ result.FailCount++;
+ result.Errors.Add(new ProductBatchImportOnlineErrorDto
+ {
+ Index = index,
+ ProductName = vo.ProductName,
+ Message = ex.Message
+ });
+ }
+ }
+
+ return result;
+ }
+
+ ///
public async Task UpdateProductsBulkAsync(
[FromBody] ProductBulkUpdateInputVo input)
{
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 5d90a76..4c7920c 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
@@ -138,6 +138,11 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
scopeLocationIds = assigned.Select(x => x.Id).ToList();
}
+ // 库中存展开 Guid;全选时编辑回显折叠为 ["ALL"],与新增传 ALL 对称
+ (regionIds, scopeLocationIds, assigned) =
+ await TeamMemberScopeDisplayHelper.CollapseScopeIdsToAllSentinelForEditAsync(
+ _dbContext.SqlSugarClient, partnerIds, regionIds, scopeLocationIds, assigned);
+
return new TeamMemberGetOutputDto
{
Id = user.Id,
@@ -155,7 +160,63 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
};
}
- ///
+ ///
+ /// 新增成员并写入 userlocation 门店绑定。
+ ///
+ ///
+ /// 范围传参支持 ALL 哨兵(大小写不敏感),与具体 Guid 同传时以 ALL 为准(全选)。
+ ///
+ /// 示例请求(Locations 全选):
+ /// ```json
+ /// {
+ /// "fullName": "Jane Doe",
+ /// "userName": "jane@example.com",
+ /// "password": "Pass123!",
+ /// "roleId": "ROLE_GUID",
+ /// "partnerId": "PARTNER_GUID",
+ /// "locationIds": ["ALL"],
+ /// "state": true
+ /// }
+ /// ```
+ ///
+ /// 示例请求(Region 全选):
+ /// ```json
+ /// {
+ /// "fullName": "John Doe",
+ /// "userName": "john@example.com",
+ /// "password": "Pass123!",
+ /// "roleId": "ROLE_GUID",
+ /// "partnerId": "PARTNER_GUID",
+ /// "regionIds": ["ALL"],
+ /// "state": true
+ /// }
+ /// ```
+ ///
+ /// 参数说明:
+ /// - partnerId / partnerIds: 展开 ALL 时的 Company 上下文(必填)
+ /// - locationIds / locations: 可含 ALL;按该公司全部门店落库;列表 assignedLocations 展示 All Location
+ /// - regionIds / groupIds: 可含 ALL;为 ALL 且同时有具体 locationIds 时以门店为准;含具体 Guid 时按 Region 展开(忽略同传 locationIds)
+ /// - 优先级:location ALL →(具体 location 且 region 空/ALL)→ region ALL → 具体 region → 具体 location → 仅 Company
+ ///
+ /// 示例请求(编辑多 Region + 同传 location,按 Region 展开):
+ /// ```json
+ /// {
+ /// "fullName": "Jane Doe",
+ /// "userName": "jane@example.com",
+ /// "roleId": "ROLE_GUID",
+ /// "partnerId": "PARTNER_GUID",
+ /// "regionIds": ["REGION_GUID_1", "REGION_GUID_2"],
+ /// "locationIds": ["LOCATION_GUID"],
+ /// "state": true
+ /// }
+ /// ```
+ /// 落库为两 Region 下门店并集;Get 回显 regionIds 含 2 个 Region。
+ ///
+ /// 新增成员请求体
+ /// 创建后的成员详情(含 assignedLocations 展示文案)
+ /// 创建成功
+ /// 参数不合法或范围无法解析(如 ALL 未传 Company)
+ /// 服务器错误
public async Task CreateAsync(TeamMemberCreateInputVo input)
{
var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input, input.RoleId);
@@ -186,7 +247,45 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
return await GetAsync(user.Id);
}
- ///
+ ///
+ /// 更新成员信息与 userlocation 门店绑定。
+ ///
+ ///
+ /// 范围传参与 一致:locationIds / regionIds / groupIds / locations 可含 ALL 哨兵;
+ /// 与具体 Guid 同传时以 ALL 为准。含具体 regionIds 时按 Region 展开落库并忽略同传 locationIds。
+ /// 覆盖 Company 全部门店时列表/详情 assignedLocations 展示 All Location。
+ ///
+ /// 示例请求(Locations ALL):
+ /// ```json
+ /// {
+ /// "fullName": "Jane Doe",
+ /// "userName": "jane@example.com",
+ /// "roleId": "ROLE_GUID",
+ /// "partnerId": "PARTNER_GUID",
+ /// "locationIds": ["ALL"],
+ /// "state": true
+ /// }
+ /// ```
+ ///
+ /// 示例请求(多 Region + 同传 location,按 Region 展开):
+ /// ```json
+ /// {
+ /// "fullName": "Jane Doe",
+ /// "userName": "jane@example.com",
+ /// "roleId": "ROLE_GUID",
+ /// "partnerId": "PARTNER_GUID",
+ /// "regionIds": ["REGION_GUID_1", "REGION_GUID_2"],
+ /// "locationIds": ["LOCATION_GUID"],
+ /// "state": true
+ /// }
+ /// ```
+ ///
+ /// 成员主键
+ /// 更新请求体
+ /// 更新后的成员详情
+ /// 更新成功
+ /// 参数不合法或成员不存在
+ /// 服务器错误
public async Task UpdateAsync(Guid id, TeamMemberUpdateInputVo input)
{
var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input);
@@ -449,6 +548,59 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
}
///
+ [HttpPost("team-member/batch-import-online")]
+ public async Task BatchImportOnlineAsync(
+ [FromBody] TeamMemberBatchImportOnlineInputVo input)
+ {
+ if (input?.Items is null || input.Items.Count == 0)
+ {
+ throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)");
+ }
+
+ var opt = _batchImportOptions.Value;
+ var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows;
+ if (input.Items.Count > maxRows)
+ {
+ throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交");
+ }
+
+ var defaultPassword = opt.TeamMemberImportDefaultPassword?.Trim() ?? string.Empty;
+ var result = new TeamMemberBatchImportOnlineResultDto();
+ for (var index = 0; index < input.Items.Count; index++)
+ {
+ var vo = input.Items[index];
+ try
+ {
+ if (string.IsNullOrWhiteSpace(vo.Password))
+ {
+ if (string.IsNullOrEmpty(defaultPassword))
+ {
+ throw new UserFriendlyException(
+ "未配置默认导入密码 FoodLabeling:BatchImport:TeamMemberImportDefaultPassword");
+ }
+
+ vo.Password = defaultPassword;
+ }
+
+ await CreateAsync(vo);
+ result.SuccessCount++;
+ }
+ catch (UserFriendlyException ex)
+ {
+ result.FailCount++;
+ result.Errors.Add(new TeamMemberBatchImportOnlineErrorDto
+ {
+ Index = index,
+ UserName = vo.UserName,
+ Message = ex.Message
+ });
+ }
+ }
+
+ return result;
+ }
+
+ ///
public async Task UpdateTeamMembersBulkAsync(
[FromBody] TeamMemberBulkUpdateInputVo input)
{
@@ -801,6 +953,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
+ // 与详情一致:全选时 regionIds/locationIds 回显 ["ALL"]
+ var rawLocationIds = (assigned ?? new List())
+ .Select(x => x.Id)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x!.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ if (rawLocationIds.Count == 0)
+ {
+ rawLocationIds = locationIdList;
+ }
+
+ (regionIds, locationIdList, assignedLocations) =
+ await TeamMemberScopeDisplayHelper.CollapseScopeIdsToAllSentinelForEditAsync(
+ _dbContext.SqlSugarClient, partnerIds, regionIds, rawLocationIds, assignedLocations);
+
items.Add(new TeamMemberGetListOutputDto
{
Id = u.Id,
@@ -844,19 +1012,6 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
: 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)
- {
- var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(
- _dbContext.SqlSugarClient, partnerIds);
- if (allRegions.Count > 0)
- {
- regionIds = allRegions;
- }
- }
-
result[userId] = new TeamMemberScopeIds
{
PartnerIds = partnerIds,
@@ -868,7 +1023,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
}
///
- /// Company Admin 列表/详情:展示所选 Company 下全部 Region 与门店。
+ /// 绑定覆盖 Company 下全部门店时,列表/详情展开为全部 Region 并折叠 Location 为 All Location 展示。
///
private async Task<(List RegionIds, List AssignedLocations)>
ApplyCompanyAdminDisplayScopeAsync(
@@ -877,16 +1032,32 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
List regionIds,
List assigned)
{
- if (!await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) ||
- partnerIds.Count == 0)
+ if (partnerIds.Count == 0)
+ {
+ return (regionIds, assigned);
+ }
+
+ var assignedIds = assigned
+ .Select(x => x.Id)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
+ if (assignedIds.Count == 0)
{
return (regionIds, assigned);
}
- var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(
- _dbContext.SqlSugarClient, partnerIds);
var allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(
_dbContext.SqlSugarClient, partnerIds);
+ if (allLocationIds.Count == 0 ||
+ !AllScopeBindingHelper.IsFullIdSelection(assignedIds, allLocationIds))
+ {
+ return (regionIds, assigned);
+ }
+
+ var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(
+ _dbContext.SqlSugarClient, partnerIds);
var allAssigned = await BuildAssignedLocationDtosAsync(allLocationIds);
return (
@@ -942,7 +1113,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
PartnerIds = input.PartnerIds,
RegionIds = input.RegionIds,
GroupIds = input.GroupIds,
- LocationIds = input.LocationIds
+ LocationIds = input.LocationIds,
+ Locations = input.Locations
}, input.RoleId);
private async Task> ResolveTeamMemberLocationIdsForSaveAsync(
@@ -951,12 +1123,16 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
{
var partnerIds = NormalizePartnerIds(input);
var regionIds = NormalizeRegionIds(input);
- var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
+ var mergedLocationInputs = MergeLocationScopeInputs(input);
+ var locationHasAll = LocationScopeBindingHelper.ContainsAllScopeSentinel(mergedLocationInputs);
+ var explicitLocationIds = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedLocationInputs);
+ var regionHasAll = LocationScopeBindingHelper.ContainsAllScopeSentinel(regionIds);
var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(
_dbContext.SqlSugarClient, roleId);
if (isCompanyAdmin && partnerIds.Count > 0 &&
- regionIds.Count == 0 && explicitLocationIds.Count == 0)
+ !regionHasAll && regionIds.Count == 0 &&
+ !locationHasAll && explicitLocationIds.Count == 0)
{
var fromPartner = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
_dbContext.SqlSugarClient, partnerIds, null, null);
@@ -969,8 +1145,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
return fromPartner;
}
- var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
- _dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds);
+ var merged = await LocationScopeBindingHelper.ResolveTeamMemberLocationIdsForSaveAsync(
+ _dbContext.SqlSugarClient, partnerIds, regionIds, mergedLocationInputs);
if (merged.Count == 0)
{
@@ -984,6 +1160,23 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
return merged;
}
+ /// 合并 与 (均可含 ALL)。
+ private static List? MergeLocationScopeInputs(TeamMemberCreateInputVo input)
+ {
+ var merged = new List();
+ if (input.LocationIds is { Count: > 0 })
+ {
+ merged.AddRange(input.LocationIds);
+ }
+
+ if (input.Locations is { Count: > 0 })
+ {
+ merged.AddRange(input.Locations);
+ }
+
+ return merged.Count > 0 ? merged : null;
+ }
+
private static List NormalizePartnerIds(TeamMemberCreateInputVo input)
{
var merged = new HashSet(StringComparer.Ordinal);
@@ -1020,7 +1213,11 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
{
var now = DateTime.Now;
var userIdString = userId.ToString();
- var wanted = locationIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList();
+ var wanted = locationIds
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
var currentUserId = CurrentUser?.Id?.ToString();
var validCount = await _dbContext.SqlSugarClient.Queryable()
@@ -1032,17 +1229,24 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
throw new UserFriendlyException("存在无效门店,请刷新后重试");
}
+ // 含已逻辑删除行:唯一键 UK_userlocation_user_location 覆盖全历史,不能对已删行再 INSERT
var existing = await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.UserId == userIdString)
.ToListAsync();
- var existingActive = existing.Where(x => !x.IsDeleted).ToList();
- var existingActiveSet = existingActive.Select(x => x.LocationId).ToHashSet();
+ var byLocation = existing
+ .GroupBy(x => (x.LocationId ?? string.Empty).Trim(), StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(g => g.Key, g => g.OrderBy(x => x.IsDeleted).ThenByDescending(x => x.CreationTime).First(), StringComparer.OrdinalIgnoreCase);
- var toDelete = existingActive.Where(x => !wanted.Contains(x.LocationId)).ToList();
- if (toDelete.Count > 0)
+ var wantedSet = wanted.ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ var toSoftDelete = existing
+ .Where(x => !x.IsDeleted && !wantedSet.Contains((x.LocationId ?? string.Empty).Trim()))
+ .Select(x => x.Id)
+ .Distinct()
+ .ToList();
+ if (toSoftDelete.Count > 0)
{
- var ids = toDelete.Select(x => x.Id).ToList();
await _dbContext.SqlSugarClient.Updateable()
.SetColumns(x => new UserLocationDbEntity
{
@@ -1050,14 +1254,25 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
LastModificationTime = now,
LastModifierId = currentUserId
})
- .Where(x => ids.Contains(x.Id))
+ .Where(x => toSoftDelete.Contains(x.Id))
.ExecuteCommandAsync();
}
- var toInsert = wanted.Where(x => !existingActiveSet.Contains(x)).ToList();
- if (toInsert.Count > 0)
+ var toReviveIds = new List();
+ var toInsert = new List();
+ foreach (var locationId in wanted)
{
- var rows = toInsert.Select(locationId => new UserLocationDbEntity
+ if (byLocation.TryGetValue(locationId, out var row))
+ {
+ if (row.IsDeleted)
+ {
+ toReviveIds.Add(row.Id);
+ }
+
+ continue;
+ }
+
+ toInsert.Add(new UserLocationDbEntity
{
Id = _guidGenerator.Create().ToString(),
IsDeleted = false,
@@ -1066,9 +1281,25 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
UserId = userIdString,
LocationId = locationId,
ConcurrencyStamp = string.Empty
- }).ToList();
+ });
+ }
- await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
+ if (toReviveIds.Count > 0)
+ {
+ await _dbContext.SqlSugarClient.Updateable()
+ .SetColumns(x => new UserLocationDbEntity
+ {
+ IsDeleted = false,
+ LastModificationTime = now,
+ LastModifierId = currentUserId
+ })
+ .Where(x => toReviveIds.Contains(x.Id))
+ .ExecuteCommandAsync();
+ }
+
+ if (toInsert.Count > 0)
+ {
+ await _dbContext.SqlSugarClient.Insertable(toInsert).ExecuteCommandAsync();
}
}
}
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 31cdd9f..bb6dd53 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,8 +20,8 @@
},
//应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049)
"App": {
- "SelfUrl": "http://192.168.31.88:19001",
- "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:5174;http://localhost:5173;http://localhost:3000"
+ "SelfUrl": "http://192.168.31.88:19003",
+ "CorsOrigins": "http://localhost:19003;http://localhost:18000;http://localhost:5666;http://localhost:5174;http://localhost:5173;http://localhost:3000"
},
//配置
"Settings": {
@@ -32,7 +32,7 @@
"DbList": [ "Sqlite", "Mysql", "Sqlserver", "Oracle", "PostgreSQL" ],
"DbConnOptions": {
- "Url": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-us;uid=netteam;pwd=netteam;CharSet=utf8mb4;",
+ "Url": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-test-us;uid=netteam;pwd=netteam;CharSet=utf8mb4;",
"DbType": "Mysql",
"EnabledReadWrite": false,//是否启用读写分离
"EnabledCodeFirst": false,//是否通过代码优先创建数据库表结构
diff --git a/项目相关文档/2026-07-23泰额版平台与公司登录拉菜单用法.md b/项目相关文档/2026-07-23泰额版平台与公司登录拉菜单用法.md
new file mode 100644
index 0000000..3f9893e
--- /dev/null
+++ b/项目相关文档/2026-07-23泰额版平台与公司登录拉菜单用法.md
@@ -0,0 +1,402 @@
+# 2026-07-23 泰额版:平台 / 公司登录与拉菜单用法
+
+本文说明泰额版 **平台管理员** 与 **公司账号** 应如何登录、如何拉菜单,以及完整传参 / 反参示例。
+
+> 相关文档:
+> - `2026-07-23泰额版当前登录账号菜单接口.md`
+> - `2026-07-23泰额版平台端操作公司级账号逻辑.md`
+
+---
+
+## 一、先分清两套身份
+
+| 角色 | 登录接口 | 用户所在库 | 拉菜单接口 | 菜单数据来源 |
+|------|----------|------------|------------|--------------|
+| **平台管理员** | `POST /api/app/th-web-auth/login`(选 **Default** + 平台邮箱)或 `POST /api/app/account/login` | 主库 `antis-foodlabeling-host`.`user` | `GET /api/app/account/Vue3Router/vben5`(H5 实际调用) | 主库 `menu`(`MenuSource=0`,后端 vben5 空时回退 Ruoyi) |
+| **公司账号** | `POST /api/app/th-web-auth/login`(选具体公司 tenantId) | 该公司业务库 `user` | `GET /api/app/auth-session/my-menus` 或 H5 侧 `Vue3Router/vben5` | **当前租户业务库** `menu` |
+
+**H5 同一登录框(2026-07-23 后端兼容):**
+
+- 前端**始终**打 `POST /api/app/th-web-auth/login`,不会打 `account/login`。
+- 下拉选 **Default**(`tenantId=11111111-1111-1111-1111-111111111111`)+ 邮箱 `admin@example.com` → 后端在主库校验,签发**无 TenantId** 的平台 Token,`tenantName` 返回 `Platform`。
+- 下拉选 **具体公司**(如「中国麦当劳公司」)→ 仍在该公司业务库校验,JWT 含 `TenantId`。
+- Default 租户业务库可能指向 `antis-foodlabeling-us`;平台邮箱账号**不会**误入 US 库。
+
+**禁止混用:**
+
+- 公司 Token 不要用平台-only 的管理接口(无租户上下文)
+- 平台 Token 调 `auth-session/my-menus` 现已可读主库菜单(兼容);H5 仍优先 `Vue3Router/vben5`
+
+Base URL 示例:`http://127.0.0.1:19002`(以部署为准)。
+
+```mermaid
+flowchart TB
+ subgraph platform [平台管理员]
+ P1[POST account/login]
+ P2[GET account/Vue3Router/ruoyi]
+ PHost[(antis-foodlabeling-host)]
+ P1 --> PHost
+ P2 --> PHost
+ end
+ subgraph company [公司账号]
+ C1[POST th-web-auth/login]
+ C2[GET auth-session/my-menus]
+ CBiz[(租户业务库)]
+ C1 --> CBiz
+ C2 --> CBiz
+ end
+```
+
+---
+
+## 二、平台管理员
+
+### 2.1 登录
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `POST /api/app/account/login` |
+| Content-Type | `application/json` |
+| 说明 | 校验主库用户;登录框按 **邮箱** 匹配(`Email` 优先,其次 `UserName`);值须含 `@` |
+
+#### 传参(`LoginInputVo`)
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `userName` | string | 是 | **填邮箱**,例如种子账号 `admin@example.com`(字段名虽为 userName) |
+| `password` | string | 是 | 明文密码;种子默认 `123456` |
+| `uuid` | string | 视环境 | 验证码会话 Id;未开验证码可省略 |
+| `code` | string | 视环境 | 图形验证码;未开验证码可省略 |
+
+```json
+{
+ "userName": "admin@example.com",
+ "password": "123456"
+}
+```
+
+#### 反参(`LoginOutputDto`)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `token` | string | JWT(后续请求 `Authorization: Bearer {token}`) |
+| `refreshToken` | string | 刷新令牌 |
+
+```json
+{
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "..."
+}
+```
+
+> 若全局包装了统一响应,实际可能是 `{ "data": { "token": "...", "refreshToken": "..." } }`,以 Swagger / 实际返回为准。
+
+```bash
+curl -X POST "http://127.0.0.1:19002/api/app/account/login" \
+ -H "Content-Type: application/json" \
+ -d "{\"userName\":\"admin@example.com\",\"password\":\"123456\"}"
+```
+
+### 2.2 拉菜单(路由)
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `GET /api/app/account/Vue3Router/ruoyi` |
+| Header | `Authorization: Bearer {token}` |
+| 说明 | 将当前用户菜单转为 **Ruoyi** 路由树。主库种子菜单 `MenuSource=0`,须用 **`ruoyi`**,不要用 `vben5`(否则按 `MenuSource=2` 过滤会空) |
+
+路径参数 `routerType`:
+
+| 值 | 对应 MenuSource | 何时用 |
+|----|-----------------|--------|
+| `ruoyi` | 0 | **当前平台种子推荐** |
+| `pure` | 1 | Pure 前端 |
+| `vben5` | 2 | 仅当菜单已标成 Vben5 时 |
+
+#### 传参
+
+无 Query Body;路径上带 `ruoyi` 即可。
+
+#### 反参
+
+返回 **前端路由结构数组/对象**(由 `Vue3RuoYiRouterBuild` 生成),不是 `auth-session/my-menus` 那种 `menus` 树。字段随框架版本可能含:`name`、`path`、`component`、`meta`、`children` 等。以实际 JSON / Swagger 为准。
+
+超级管理员(用户名 `admin`)会拿到对应 `MenuSource` 下全部未删菜单再构建路由。
+
+```bash
+curl -G "http://127.0.0.1:19002/api/app/account/Vue3Router/ruoyi" \
+ -H "Authorization: Bearer "
+```
+
+### 2.3 平台菜单数据位置
+
+| 库表 | 说明 |
+|------|------|
+| `antis-foodlabeling-host`.`menu` | 约 26 条:首页概览、平台管理/SAAS 公司、标签管理…、管理/账户管理… |
+| `user` / `role` / `rolemenu` | 平台 `admin` 已绑定上述菜单 |
+
+种子脚本:`th-tenant-menu-seed.sql`(menu)、`th-platform-user-role-seed.sql`(user/role)。菜单 Id 均为合法 Guid(见下文「平台应有菜单清单」)。
+
+---
+
+## 七、平台应有菜单清单
+
+主库 `antis-foodlabeling-host`.`menu` 共 **26** 条未删食品 SaaS 菜单,`MenuSource=0`(Ruoyi)。根菜单 `ParentId='0'`;子菜单 `ParentId` 为父目录 Guid。
+
+| 菜单名称 | Id | router | routerName | component | menuType | parentId | orderNum | menuIcon |
+|---|---|---|---|---:|---|---:|---:|---|
+| 首页概览 | `f0010001-0001-4000-8000-000000000001` | `/analytics` | `FoodLabelingDashboard` | `/food-labeling/dashboard/index` | 1 | `0` | -1 | `lucide:layout-dashboard` |
+| 平台管理 | `f0010002-0001-4000-8000-000000000002` | `/platform` | `FoodLabelingPlatform` | 空 | 0 | `0` | 5 | `lucide:cloud-cog` |
+| SAAS 公司 | `f0010003-0001-4000-8000-000000000003` | `/platform/tenants` | `FoodLabelingPlatformTenants` | `/food-labeling/platform/tenants/index` | 1 | `f0010002-0001-4000-8000-000000000002` | 6 | `lucide:building` |
+| 标签管理 | `f0010010-0001-4000-8000-000000000010` | `/labeling` | `FoodLabelingLabeling` | 空 | 0 | `0` | 10 | `lucide:tags` |
+| 标签 | `f0010011-0001-4000-8000-000000000011` | `/labels` | `FoodLabelingLabels` | `/food-labeling/labeling/labels/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 11 | `lucide:tag` |
+| 标签分类 | `f0010012-0001-4000-8000-000000000012` | `/label-categories` | `FoodLabelingLabelCategories` | `/food-labeling/labeling/label-categories/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 12 | `lucide:folder-tree` |
+| 标签类型 | `f0010013-0001-4000-8000-000000000013` | `/label-types` | `FoodLabelingLabelTypes` | `/food-labeling/labeling/label-types/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 13 | `lucide:layers` |
+| 标签模板 | `f0010014-0001-4000-8000-000000000014` | `/label-templates` | `FoodLabelingLabelTemplates` | `/food-labeling/labeling/label-templates/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 14 | `lucide:layout-template` |
+| 多选选项集 | `f0010015-0001-4000-8000-000000000015` | `/multiple-options` | `FoodLabelingMultipleOptions` | `/food-labeling/labeling/multiple-options/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 15 | `lucide:list-checks` |
+| 业务模块 | `f0010020-0001-4000-8000-000000000020` | `/modules` | `FoodLabelingModules` | 空 | 0 | `0` | 15 | `lucide:boxes` |
+| 培训 | `f0010021-0001-4000-8000-000000000021` | `/training` | `FoodLabelingTraining` | `/food-labeling/modules/training/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 16 | `lucide:graduation-cap` |
+| 告警 | `f0010022-0001-4000-8000-000000000022` | `/alerts` | `FoodLabelingAlerts` | `/food-labeling/modules/alerts/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 17 | `lucide:bell` |
+| 任务 | `f0010023-0001-4000-8000-000000000023` | `/tasks` | `FoodLabelingTasks` | `/food-labeling/modules/tasks/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 18 | `lucide:list-todo` |
+| 传感器 | `f0010024-0001-4000-8000-000000000024` | `/sensors` | `FoodLabelingSensors` | `/food-labeling/modules/sensors/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 19 | `lucide:activity` |
+| 食物浪费 | `f0010025-0001-4000-8000-000000000025` | `/food-waste` | `FoodLabelingFoodWaste` | `/food-labeling/modules/food-waste/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 20 | `lucide:apple` |
+| 电子标签 | `f0010026-0001-4000-8000-000000000026` | `/e-label-module` | `FoodLabelingELabelModule` | `/food-labeling/modules/e-label/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 21 | `lucide:file-digit` |
+| 管理 | `f0010030-0001-4000-8000-000000000030` | `/management` | `FoodLabelingManagement` | 空 | 0 | `0` | 20 | `lucide:building-2` |
+| 账户管理 | `f0010031-0001-4000-8000-000000000031` | `/account-management` | `FoodLabelingAccountManagement` | `/food-labeling/management/account-management/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 21 | `lucide:users` |
+| 系统菜单 | `f0010032-0001-4000-8000-000000000032` | `/system-menu` | `FoodLabelingSystemMenu` | `/food-labeling/management/system-menu/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 22 | `lucide:menu-square` |
+| 菜单管理 | `f0010033-0001-4000-8000-000000000033` | `/menu-management` | `FoodLabelingMenuManagement` | `/food-labeling/management/menu-management/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 23 | `lucide:utensils` |
+| 设备 | `f0010034-0001-4000-8000-000000000034` | `/devices` | `FoodLabelingDevices` | `/food-labeling/modules/devices/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 24 | `lucide:smartphone` |
+| 报表 | `f0010035-0001-4000-8000-000000000035` | `/reports` | `FoodLabelingReports` | `/food-labeling/management/reports/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 25 | `lucide:file-bar-chart` |
+| 发票 | `f0010036-0001-4000-8000-000000000036` | `/invoices` | `FoodLabelingInvoices` | `/food-labeling/modules/invoices/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 26 | `lucide:receipt` |
+| 二维码 | `f0010037-0001-4000-8000-000000000037` | `/qr-codes` | `FoodLabelingQrCodes` | `/food-labeling/modules/qr-codes/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 27 | `lucide:qr-code` |
+| 支持 | `f0010038-0001-4000-8000-000000000038` | `/support` | `FoodLabelingSupport` | `/food-labeling/management/support/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 28 | `lucide:life-buoy` |
+| API | `f0010039-0001-4000-8000-000000000039` | `/api-settings` | `FoodLabelingApi` | `/food-labeling/modules/api/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 29 | `lucide:code-2` |
+
+平台 admin 账号:`UserName=admin`,`Email=admin@example.com`,`Id=f0020001-0001-4000-8000-000000000001`;角色 `Id=f0020002-0001-4000-8000-000000000002`;`rolemenu` 绑定上述 26 条菜单。
+
+---
+
+## 三、公司账号
+
+### 3.1 登录
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `POST /api/app/th-web-auth/login` |
+| Content-Type | `application/json` |
+| 说明 | 须指定租户;在**该公司业务库**校验账号,JWT 含 `TenantId` |
+
+#### 传参(`ThWebLoginInputVo`)
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `tenantId` | guid | 与 `tenantName` 二选一 | 租户 Id(推荐) |
+| `tenantName` | string | 与 `tenantId` 二选一 | 租户名称,如 `中国麦当劳公司` |
+| `userName` | string | 是 | 登录账号(可为邮箱或用户名) |
+| `password` | string | 是 | 明文密码 |
+| `uuid` / `code` | string | 视环境 | 验证码 |
+
+```json
+{
+ "tenantId": "3a229a02-77eb-1dcc-afb6-09e2a2b6386c",
+ "userName": "admin",
+ "password": "123456"
+}
+```
+
+或:
+
+```json
+{
+ "tenantName": "中国麦当劳公司",
+ "userName": "admin",
+ "password": "123456"
+}
+```
+
+#### 反参(`ThWebLoginOutputDto`)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `token` | string | JWT(含租户与 RBAC Claims) |
+| `refreshToken` | string | 刷新令牌 |
+| `tenantId` | guid | 当前租户 Id |
+| `tenantName` | string | 当前租户名称 |
+
+```json
+{
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "...",
+ "tenantId": "3a229a02-77eb-1dcc-afb6-09e2a2b6386c",
+ "tenantName": "中国麦当劳公司"
+}
+```
+
+```bash
+curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
+ -H "Content-Type: application/json" \
+ -d "{\"tenantId\":\"3a229a02-77eb-1dcc-afb6-09e2a2b6386c\",\"userName\":\"admin\",\"password\":\"123456\"}"
+```
+
+### 3.2 拉菜单
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `GET /api/app/auth-session/my-menus` |
+| Header | `Authorization: Bearer {公司 token}` |
+| 建议 Header | `__tenant: {tenantId}`(与 JWT 中 TenantId 一致更稳妥) |
+| 说明 | 读**当前租户业务库** `menu` + 角色;用户名为 `admin` 时返回该库全部未删菜单 |
+
+#### 传参
+
+无 Body;依赖 Token(及可选 `__tenant`)。
+
+#### 反参(`CurrentUserMenuPermissionsOutputDto`,字段一般为 camelCase)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `user` | object | 当前用户简要信息 |
+| `user.id` | guid/string | 用户 Id |
+| `user.userName` | string | 登录名 |
+| `user.nick` | string? | 昵称 |
+| `user.email` | string? | 邮箱 |
+| `user.icon` | string? | 头像 |
+| `roleCodes` | string[] | 角色编码 |
+| `permissionCodes` | string[] | 权限码(超管常见 `*:*:*`) |
+| `accessPermissionCodes` | string[] | 角色访问权限编码 |
+| `menus` | array | **菜单树**(根列表,`children` 嵌套) |
+| `role` | string | 角色展示名 |
+| `fullName` | string | 全名 |
+| `lastUpdated` | datetime? | 供前端刷新缓存 |
+
+**菜单节点**(`CurrentUserMenuNodeDto`)常见字段:
+
+| 字段 | 说明 |
+|------|------|
+| `id` / `parentId` | 菜单 Id、父 Id(根多为 `"0"`) |
+| `menuName` | 名称 |
+| `router` / `routerName` / `component` | 路由与组件 |
+| `menuType` / `menuSource` / `orderNum` | 类型、来源、排序 |
+| `permissionCode` | 权限码 |
+| `menuIcon` / `isShow` / `state` | 图标、显示、启用 |
+| `children` | 子节点数组 |
+
+```json
+{
+ "user": {
+ "id": "...",
+ "userName": "admin",
+ "nick": "超级管理员",
+ "email": "admin@example.com"
+ },
+ "roleCodes": ["admin"],
+ "permissionCodes": ["*:*:*"],
+ "accessPermissionCodes": [],
+ "menus": [
+ {
+ "id": "...",
+ "parentId": "0",
+ "menuName": "系统管理",
+ "router": "/system",
+ "orderNum": 100,
+ "children": []
+ }
+ ],
+ "role": "管理员",
+ "fullName": "超级管理员",
+ "lastUpdated": "2026-07-23T10:00:00"
+}
+```
+
+```bash
+curl -G "http://127.0.0.1:19002/api/app/auth-session/my-menus" \
+ -H "Authorization: Bearer " \
+ -H "__tenant: 3a229a02-77eb-1dcc-afb6-09e2a2b6386c"
+```
+
+未识别租户时典型错误:
+
+> 未识别租户上下文。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 `__tenant` 携带租户 Id。
+
+### 3.3 公司库菜单现状说明
+
+| 库 | 当前菜单大致情况 |
+|----|------------------|
+| 主库 host | 已种食品 SaaS 中文菜单(平台用) |
+| Default → `antis-foodlabeling-us` | 美国版英文业务菜单 |
+| 如 `antis-foodlabeling-t46d56528`(中国麦当劳) | 多为 **Yi 框架**种子(系统管理/用户管理…),**尚未**种 `th-tenant-menu-seed.sql` 那套食品 SaaS 菜单 |
+
+若公司端侧边栏也要与平台同一套「首页概览 / 标签管理 / 业务模块…」:
+
+1. 对该公司业务库执行 `项目相关文档/th-tenant-menu-seed.sql`(**禁止**对 `antis-foodlabeling-us` 执行,以免污染美国版)
+2. 为公司管理员角色绑定对应 `rolemenu`(或公司 `admin` 用户名可直接看全库未删菜单)
+3. 再用 `th-web-auth/login` + `auth-session/my-menus` 验证
+
+---
+
+## 四、对照速查
+
+| 步骤 | 平台管理员(H5 选 Default) | 公司账号 |
+|------|------------|----------|
+| 1. 登录 | `POST /api/app/th-web-auth/login`(Default tenantId + 邮箱) | `POST /api/app/th-web-auth/login` |
+| 2. 关键 | Default `tenantId` + 邮箱 `userName` + `password` | 公司 `tenantId` + `userName` + `password` |
+| 3. 登录反参 | `token` / `refreshToken` / `tenantName=Platform` | `token` / `refreshToken` / `tenantId` / `tenantName` |
+| 4. 拉菜单(H5) | `GET /api/app/account/Vue3Router/vben5` | `GET /api/app/auth-session/my-menus` 或 H5 同 vben5 |
+| 5. 菜单 Header | `Authorization: Bearer …` | `Authorization` + 建议 `__tenant` |
+| 6. 菜单反参形态 | Vben5 路由树(Ruoyi 回退) | `user` + `menus` 树 + 权限码 |
+
+---
+
+## 五、前端对接注意(H5 / Vben5,只读不改源码)
+
+1. **登录**:`POST /api/app/th-web-auth/login`(见 `th-web-auth.ts`),Header 带 `__tenant: tenantId`。
+2. **登录 Body**:`tenantId`、`userName`(Email 字段)、`password`;可选 `uuid`/`code`(验证码)。
+3. **tenant-select**:`GET /api/app/th-multi-tenancy/tenant-select` 现返回 `id` + `name`;前端用 `id` 作为下拉 value。
+4. **平台管理员操作**:选 **Default**,邮箱填 `admin@example.com`,密码 `123456`。
+5. **公司操作**:选 **中国麦当劳公司** 等公司项,填该公司业务库账号(如 `admin`)。
+6. **拉菜单**:H5 dist 实际调 `GET /api/app/account/Vue3Router/vben5`(非 `my-menus`);平台 Token 无租户即可;主库 `MenuSource=0` 时后端自动回退 Ruoyi 再 Vben5 构建。
+7. **公司端**仍可用 `auth-session/my-menus`(须 Token + `__tenant`)。
+8. 统一响应包装时从 `data` 取 `token` / `menus`。
+
+### 5.1 H5 登录请求示例(平台)
+
+```json
+POST /api/app/th-web-auth/login
+Header: __tenant: 11111111-1111-1111-1111-111111111111
+Body:
+{
+ "tenantId": "11111111-1111-1111-1111-111111111111",
+ "userName": "admin@example.com",
+ "password": "123456"
+}
+```
+
+成功反参(平台):
+
+```json
+{
+ "token": "eyJ...",
+ "refreshToken": "...",
+ "tenantId": "00000000-0000-0000-0000-000000000000",
+ "tenantName": "Platform"
+}
+```
+
+### 5.2 H5 登录请求示例(公司)
+
+```json
+{
+ "tenantId": "3a229a02-77eb-1dcc-afb6-09e2a2b6386c",
+ "userName": "admin",
+ "password": "123456"
+}
+```
+
+---
+
+## 六、自检清单
+
+- [ ] H5 选 Default + `admin@example.com`:`th-web-auth/login` 成功,`tenantName=Platform`,Token 无 TenantId Claim
+- [ ] 平台:`Vue3Router/vben5` 返回非空路由(Ruoyi 菜单回退)
+- [ ] 公司:选公司 tenantId + 公司账号登录未破坏;`my-menus` 带 `__tenant` 成功
+- [ ] 公司业务库菜单内容符合产品预期(必要时单独执行租户库 menu 种子,避开 US 库)
diff --git a/项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md b/项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md
new file mode 100644
index 0000000..8b204da
--- /dev/null
+++ b/项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md
@@ -0,0 +1,261 @@
+# 2026-07-23 泰额版:平台端操作公司级账号逻辑说明
+
+本文档说明 **泰额版** 平台管理端如何操作「公司级」账号与权限:架构、接口、写库路径、与公司端登录的关系,以及**不得影响美国版**的隔离约束。
+
+相关文档:
+
+- `2026-07-21代码优化.md`:公司列表 / 改密 / SaaS 菜单与角色菜单接口细节
+- `2026-07-23泰额版当前登录账号菜单接口.md`:平台 vs 公司「当前登录人菜单」
+- `th-tenant-menu-seed.sql` / `th-platform-user-role-seed.sql`:平台主库菜单与 User/Role 种子
+
+---
+
+## 一、角色与库分层
+
+| 端 | 登录 | 用户库 | 典型能力 |
+|----|------|--------|----------|
+| **平台端** | `POST /api/app/account/login` | 主库 `antis-foodlabeling-host`.`user` | 公司列表、改公司管理员、分配 SaaS 菜单、改公司角色菜单、开通/删除公司 |
+| **公司端** | `POST /api/app/th-web-auth/login`(须 `tenantId`) | 该公司业务库 `user` | 业务功能;`GET /api/app/auth-session/my-menus` |
+
+**公司 = 租户**:主库表 `YiTenant`(前端常把 `name` 当 `companyName`)。
+
+```mermaid
+flowchart LR
+ subgraph platform [平台端]
+ PLogin[account/login]
+ PHost[(antis-foodlabeling-host)]
+ PLogin --> PHost
+ end
+ subgraph company [公司端]
+ CLogin[th-web-auth/login]
+ CBiz[(租户业务库)]
+ CLogin --> CBiz
+ end
+ PHost -->|YiTenant.TenantConnectionString| CBiz
+ PHost -->|改 admin / 角色菜单| CBiz
+```
+
+### 1.1 库职责
+
+| 库 | 配置来源 | 平台操作相关表 |
+|----|----------|----------------|
+| **主库** `antis-foodlabeling-host` | `DbConnOptions.Url` | `YiTenant`、`fl_th_tenant_admin_credential`、`fl_th_tenant_menu_permission`、平台 `user`/`role`/`menu`/`userrole`/`rolemenu` |
+| **租户业务库** `antis-foodlabeling-{key}` | `YiTenant.TenantConnectionString` | 公司 `User`(登录哈希)、`Role`/`RoleMenu`/`Menu`、业务表 |
+| **美国版库** `antis-foodlabeling-us` | 美版 `appsettings`;泰额 **Default** 租户连接串也指向此库 | **禁止**当作泰额新公司业务库去写平台种子;对 Default 改 admin/角色会动到美版数据 |
+
+### 1.2 两层「菜单」勿混淆
+
+| 层 | 存储 | 接口 | 语义 |
+|----|------|------|------|
+| **SaaS 公司开通模块** | 主库 `fl_th_tenant_menu_permission`(`menuPermissionKeys`) | `menu-permission-tree` / `company-menus` | 该公司「能开哪些产品模块」 |
+| **公司 RBAC 菜单** | 租户库 `Menu` + `RoleMenu` | `company-roles` / `company-role-menus`;公司端 `auth-session/my-menus` | 该公司内角色能看哪些业务菜单 |
+
+平台主库种子菜单(`th-menu-*`)服务于**平台端自身路由**,不是公司业务库菜单。
+
+---
+
+## 二、平台端操作公司账号:能力总览
+
+实现类:`ThMultiTenancyAppService`(路由前缀 `/api/app/th-multi-tenancy/`)。
+开通公司:`ThTenantProvisioningAppService`(`/api/app/th-tenant-provisioning/`)。
+
+| 能力 | 方法 / 路径 | 主要读写 |
+|------|-------------|----------|
+| 登录页租户下拉(匿名) | `GET .../tenant-select` | 读主库租户 + 凭据(无 Id) |
+| 公司分页列表 | `GET .../company-list` | 读主库租户 + 凭据 + SaaS keys |
+| 改公司管理员账号/密码 | `PUT .../company-admin` | 写**租户库 User** + 写主库凭据表 |
+| SaaS 权限树 | `GET .../menu-permission-tree` | 代码目录,无库 |
+| 查/设公司 SaaS 菜单 | `GET/PUT .../company-menus` | 读写主库 `fl_th_tenant_menu_permission` |
+| 查公司角色 | `GET .../company-roles` | 读租户库 `Role`/`RoleMenu` |
+| 设公司角色菜单 | `PUT .../company-role-menus` | 写租户库 `RoleMenu` |
+| 开通公司 | `POST /th-tenant-provisioning/provision` | 写主库 `YiTenant` + 建业务库/Init |
+| 删除公司 | `DELETE .../company/{tenantId}` | 软删主库租户;可选 DROP 业务库 |
+
+鉴权:除 `tenant-select` 外,平台管理接口需 **平台 Token**(`account/login`)。
+
+---
+
+## 三、核心数据流
+
+### 3.1 开通公司(产生「公司级账号」)
+
+```text
+平台管理员
+ → POST /api/app/th-tenant-provisioning/provision
+ → 主库写入 YiTenant(Name + TenantConnectionString)
+ → EnsureDatabaseCreated(物理建库)
+ → 后台 InitAsync:业务库 CodeFirst + Seed
+ └─ 默认 User.UserName = admin,密码 = RbacOptions.AdminPassword(哈希)
+```
+
+要点:
+
+- 开通**不会**自动写 `fl_th_tenant_admin_credential`。
+- 列表回显密码:无凭据行时用配置默认密码现场 AES 加密;平台首次 `PUT company-admin` 后才持久化可回显密文。
+- Init 异步未完成时,立刻改 admin / 公司登录可能失败。
+
+### 3.2 平台改公司管理员(重点)
+
+接口:`PUT /api/app/th-multi-tenancy/company-admin`
+
+```text
+1. CurrentTenant=null(主库 scope)
+ 读 YiTenant、fl_th_tenant_admin_credential(当前 LoginAccount,默认 admin)
+2. 若有 password + passwordSalt:AES 解密得明文
+3. 用 YiTenant.TenantConnectionString 建独立 SqlSugarClient(直连业务库,不靠 Change(tenantId) 切仓储)
+4. 在业务库 User 上:
+ - 可选改 UserName(查重)
+ - 可选 ApplyPlainPassword → 写 Password/Salt 哈希
+5. Upsert 主库 fl_th_tenant_admin_credential(LoginAccount + AES 密文 + IV)
+```
+
+| 写入位置 | 字段语义 |
+|----------|----------|
+| 租户库 `User.Password/Salt` | **登录校验用哈希**(不可逆) |
+| 主库 `PasswordCipher/PasswordIv` | **列表回显用 AES 密文**(可逆,密钥 `FoodLabeling:TenantSelectCrypto`) |
+
+前端改密必须先按约定 AES 加密再提交,**禁止**明文密码直传。
+
+### 3.3 平台分配公司 SaaS 菜单
+
+```text
+GET menu-permission-tree → 静态 Key 树(ThSaasMenuPermissionCatalog)
+GET company-menus?tenantId= → 读主库已分配 keys
+PUT company-menus → 主库先删后插 fl_th_tenant_menu_permission
+```
+
+不写租户库 `Menu`/`RoleMenu`。非法 key 会校验失败。
+
+### 3.4 平台配置公司内角色菜单
+
+```text
+GET company-roles?tenantId= → 直连租户库读 Role + RoleMenu.menuIds
+PUT company-role-menus → 直连租户库覆盖 RoleMenu(校验 Menu 存在)
+```
+
+这与公司员工登录后看到的 `auth-session/my-menus` 同源(租户库 RBAC)。
+
+### 3.5 删除公司
+
+```text
+DELETE /api/app/th-multi-tenancy/company/{tenantId}?dropDatabase=true
+ → 禁止删除 Default 租户
+ → DROP 业务库时禁止库名 host / us(及受保护库)
+ → 软删 YiTenant
+ → 删凭据表、SaaS 菜单权限、租户缓存
+```
+
+---
+
+## 四、跨库访问约定(实现要点)
+
+| 约定 | 说明 |
+|------|------|
+| 主库表 | 实体带 `[DefaultTenantTable]`;读写前 `CurrentTenant.Change(null)`,连接走 `DbConnOptions.Url`(host) |
+| 租户业务库 | `TenantBusinessDatabaseAccessor`:按 `YiTenant` 连接串 **独立 SqlSugarClient**,避免空连接串误连主库 |
+| 不信任 JWT 租户 | 平台改公司数据时以入参 `tenantId` + 主库登记的连接串为准,不以「当前登录租户」替代目标公司 |
+
+---
+
+## 五、与美国版隔离(强制)
+
+| 规则 | 说明 |
+|------|------|
+| 平台种子只写 **host** | `menu` / `user` / `role` 等平台数据禁止写入 `antis-foodlabeling-us` |
+| Default 租户 = US 业务库 | host 中 Default 的 `TenantConnectionString` 指向 `antis-foodlabeling-us`;对该租户执行 `company-admin` / `company-role-menus` **会改美版库** |
+| 新公司必须独立库 | Provision 使用 `antis-foodlabeling-{tenant}` 模板;勿把新公司指到 `us` |
+| 删除保护 | 不可删 Default;不可 DROP `host`/`us` |
+| Token 隔离 | 平台 Token 调公司 `my-menus` 会因无租户失败;公司 Token 勿当平台管理凭证 |
+
+**建议**:平台运营界面操作公司时,只选择泰额独立业务库租户(如 `中国麦当劳公司` → `antis-foodlabeling-t46d56528`),避免点选 Default。
+
+---
+
+## 六、接口速查(含 curl)
+
+Base:`http://127.0.0.1:19002`(以环境为准)。
+
+```bash
+# 平台登录
+curl -X POST "http://127.0.0.1:19002/api/app/account/login" \
+ -H "Content-Type: application/json" \
+ -d "{\"userName\":\"admin@example.com\",\"password\":\"123456\"}"
+
+# 公司列表(含加密账号密码)
+curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-list" \
+ --data-urlencode "skipCount=0" --data-urlencode "maxResultCount=20" \
+ -H "Authorization: Bearer "
+
+# 改公司管理员(password 为前端 AES 密文)
+curl -X PUT "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-admin" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d "{\"tenantId\":\"\",\"loginAccount\":\"admin\",\"password\":\"\",\"passwordSalt\":\"\"}"
+
+# 公司 SaaS 菜单
+curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-menus" \
+ --data-urlencode "tenantId=" \
+ -H "Authorization: Bearer "
+
+# 公司角色
+curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-roles" \
+ --data-urlencode "tenantId=" \
+ -H "Authorization: Bearer "
+```
+
+公司端自测登录(验证平台改密是否生效):
+
+```bash
+curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
+ -H "Content-Type: application/json" \
+ -d "{\"tenantId\":\"\",\"userName\":\"admin\",\"password\":\"新明文密码\"}"
+```
+
+---
+
+## 七、关键代码路径
+
+| 模块 | 路径 |
+|------|------|
+| 平台多租户服务 | `FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs` |
+| 开通租户 | `FoodLabeling.Th.Application/Services/ThTenantProvisioningAppService.cs` |
+| 业务库直连 | `FoodLabeling.Th.Application/MultiTenancy/TenantBusinessDatabaseAccessor.cs` |
+| AES 凭据 | `FoodLabeling.Th.Application/MultiTenancy/TenantSelectCredentialCipher.cs` |
+| SaaS Key 目录 | `FoodLabeling.Th.Application/MultiTenancy/ThSaasMenuPermissionCatalog.cs` |
+| 主库凭据实体 | `FoodLabeling.Th.Domain/Entities/ThTenantAdminCredentialEntity.cs` |
+| 主库 SaaS 菜单实体 | `FoodLabeling.Th.Domain/Entities/ThTenantMenuPermissionEntity.cs` |
+| 公司 Web 登录 | `FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs` |
+| 平台登录 | `Yi.Framework.Rbac.Application/Services/AccountService.cs` |
+
+---
+
+## 八、风险与自检
+
+| 风险 | 应对 |
+|------|------|
+| 对 Default 改密 / 改角色 → 写 US | 运营禁操作 Default;删除已有保护,改密无同等硬拦 |
+| 凭据表可逆 AES | 保护 `TenantSelectCrypto:SecretKey`;仅平台管理员可见列表 |
+| 开通未写凭据表 | 改密走平台接口后才会与列表回显一致 |
+| Init 未完成就改 admin | 等「后台初始化完成」或调 `initialize-tenant-database` |
+| SaaS keys ≠ RBAC 菜单 | 配置完 `company-menus` 后,公司内角色仍需 `company-role-menus` / 公司端角色管理 |
+
+自检清单:
+
+- [ ] 平台用 `account/login`(host `admin`),公司用 `th-web-auth/login`
+- [ ] `company-list` 能看到目标公司 Id 与加密凭据
+- [ ] `company-admin` 后,公司端可用新密码登录;主库凭据表有对应行
+- [ ] `company-menus` 只改 host;`company-role-menus` 只改目标租户库
+- [ ] `antis-foodlabeling-us` 的 user/role 数量未因平台种子脚本变化
+- [ ] 未对 Default / `database=us` 执行 DROP
+
+---
+
+## 九、小结
+
+平台端操作公司级账号的本质是:
+
+1. **元数据与回显凭据**放在主库 host;
+2. **真正能登录的公司账号**在各公司业务库 `User`;
+3. 平台通过 `tenantId` + 直连连接串跨库改公司 admin / 角色菜单;
+4. SaaS 模块开关与公司内 RBAC 是两层模型;
+5. **永远不要把美国版库当成泰额平台种子库或随意操作 Default 租户。**
diff --git a/项目相关文档/2026-07-23泰额版当前登录账号菜单接口.md b/项目相关文档/2026-07-23泰额版当前登录账号菜单接口.md
new file mode 100644
index 0000000..d6ae522
--- /dev/null
+++ b/项目相关文档/2026-07-23泰额版当前登录账号菜单接口.md
@@ -0,0 +1,194 @@
+# 2026-07-23 泰额版:当前登录账号菜单接口
+
+本文档说明 **泰额版**「获取当前登录账号菜单」的两套接口:**平台级**与**公司级(租户)**。
+
+> 与「平台给公司配置 SaaS 菜单权限」(`menu-permission-tree` / `company-menus`)不同:
+> 本文接口返回的是**当前登录人自己可见的菜单**,用于侧边栏 / 动态路由 / 按钮权限。
+
+---
+
+## 一、对照一览
+
+| 维度 | 平台级 | 公司级(租户业务端) |
+|------|--------|----------------------|
+| 适用角色 | 平台管理员(Yi 框架 / 主库账号) | 某公司(租户)下的业务管理员 / 员工 |
+| 登录接口 | `POST /api/app/account/login` | `POST /api/app/th-web-auth/login`(须带 `tenantId`) |
+| 拉菜单接口 | `GET /api/app/account/Vue3Router/vben5` | `GET /api/app/auth-session/my-menus` |
+| 备选 | `GET /api/app/account`(用户+角色+菜单) | — |
+| 数据来源 | 当前 Account 上下文下的 `menu`(按 `MenuSource` 过滤) | **当前租户业务库** `menu` + 角色菜单 |
+| 租户上下文 | 一般**无** `__tenant` / JWT TenantId | JWT 含 `TenantId`,建议 Header 再带 `__tenant` |
+| 实现 | `AccountService.GetVue3Router` | `AuthSessionAppService.GetMyMenusAsync` |
+
+**Base URL 示例**:`http://127.0.0.1:19002`(以 `appsettings` / 部署环境为准)。
+
+---
+
+## 二、公司级:当前登录账号菜单
+
+### 2.1 接口
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `GET /api/app/auth-session/my-menus` |
+| 鉴权 | `Authorization: Bearer {token}` |
+| 租户 | Token 须来自 `th-web-auth/login`;建议同时传 Header `__tenant: {tenantId}` |
+| 说明 | 返回当前用户角色、权限码、可见菜单树;未识别租户时会报错引导使用泰额登录 |
+
+### 2.2 登录(前置)
+
+```http
+POST /api/app/th-web-auth/login
+Content-Type: application/json
+
+{
+ "tenantId": "11111111-1111-1111-1111-111111111111",
+ "userName": "admin",
+ "password": "123456"
+}
+```
+
+> 不要用 `POST /api/app/account/login` 作为泰额公司端主登录(无租户会查主库)。
+
+### 2.3 curl 示例
+
+```bash
+# 1) 登录拿 Token
+curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
+ -H "Content-Type: application/json" \
+ -d "{\"tenantId\":\"\",\"userName\":\"admin\",\"password\":\"123456\"}"
+
+# 2) 拉当前账号菜单
+curl -G "http://127.0.0.1:19002/api/app/auth-session/my-menus" \
+ -H "Authorization: Bearer " \
+ -H "__tenant: "
+```
+
+### 2.4 出参要点(`CurrentUserMenuPermissionsOutputDto`)
+
+| 字段 | 说明 |
+|------|------|
+| `user` | 当前用户简要信息 |
+| `roleCodes` | 角色编码列表 |
+| `permissionCodes` | 权限码列表(超管常见 `*:*:*`) |
+| `accessPermissionCodes` | 角色访问权限编码(如 `manage_people`) |
+| `menus` | 菜单树(`children` 嵌套;根 `parentId` 多为 `"0"`) |
+| `role` | 角色展示名 |
+| `fullName` | 全名 |
+| `lastUpdated` | 系统编辑 / 业务变更相关时间(供前端刷新缓存) |
+
+未带租户上下文时,典型错误提示:
+
+> 未识别租户上下文。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 `__tenant` 携带租户 Id。
+
+---
+
+## 三、平台级:当前登录账号菜单
+
+### 3.1 接口(推荐)
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `GET /api/app/account/Vue3Router/vben5` |
+| 鉴权 | `Authorization: Bearer {token}` |
+| 说明 | 将当前登录用户菜单转为 **Vben5** 前端路由结构 |
+
+`routerType` 路径参数:
+
+| 值 | 对应 `MenuSourceEnum` | 说明 |
+|----|----------------------|------|
+| `vben5`(推荐) | 2 = Vben5 | Vben Admin v5 路由构建 |
+| `ruoyi` | 0 = Ruoyi | 若依 Vue3 路由构建 |
+| `pure` | 1 = Pure | vue-pure-admin 路由构建 |
+| 省略 | 默认按 ruoyi | 与框架历史默认一致 |
+
+完整路径示例:
+
+- `GET /api/app/account/Vue3Router/vben5`
+- `GET /api/app/account/Vue3Router/ruoyi`
+- `GET /api/app/account/Vue3Router/pure`
+
+### 3.2 备选:账户信息(含菜单)
+
+| 项 | 值 |
+|----|------|
+| 方法 / 路径 | `GET /api/app/account` |
+| 鉴权 | 需登录 |
+| 说明 | 返回 `UserRoleMenuDto`(用户、角色、菜单等),非专门「路由树」格式 |
+
+### 3.3 登录(前置)
+
+```http
+POST /api/app/account/login
+Content-Type: application/json
+
+{
+ "userName": "admin",
+ "password": "123456"
+}
+```
+
+> 平台主库账号;一般不传 `tenantId`。字段名以 Swagger 为准(历史上 Web 登录可能按邮箱匹配)。
+
+### 3.4 curl 示例
+
+```bash
+# 1) 平台登录
+curl -X POST "http://127.0.0.1:19002/api/app/account/login" \
+ -H "Content-Type: application/json" \
+ -d "{\"userName\":\"admin\",\"password\":\"123456\"}"
+
+# 2) 拉当前账号 Vben5 路由菜单
+curl -G "http://127.0.0.1:19002/api/app/account/Vue3Router/vben5" \
+ -H "Authorization: Bearer "
+```
+
+### 3.5 行为说明
+
+- 按当前用户角色关联菜单过滤;用户名为 `admin` 时框架侧可返回对应 `MenuSource` 下全部菜单再构建路由。
+- 只消费 `menu` 表中 `MenuSource` 与 `routerType` 匹配的记录。
+
+---
+
+## 四、容易混淆的接口(不是「当前登录人菜单」)
+
+以下属于**平台给公司配置权限**,不要当成「当前登录账号菜单」:
+
+| 方法 / 路径 | 用途 |
+|-------------|------|
+| `GET /api/app/th-multi-tenancy/menu-permission-tree` | SaaS 菜单权限目录树(配置用) |
+| `GET /api/app/th-multi-tenancy/company-menus` | 查询某公司已开通的 `menuPermissionKeys` |
+| `PUT /api/app/th-multi-tenancy/company-menus` | 设置某公司 SaaS 菜单权限 |
+| `GET /api/app/th-rbac-menu/tree` | 租户业务库菜单 **CRUD/管理** 全量树(非会话权限接口) |
+
+详见:`项目相关文档/2026-07-21代码优化.md`、`项目相关文档/2026-07-22代码优化.md`。
+
+---
+
+## 五、前端对接建议
+
+| 端 | 登录后拉菜单 |
+|----|----------------|
+| 公司 / 租户 Web | `th-web-auth/login` → `auth-session/my-menus`;请求始终带 Token + `__tenant` |
+| 平台管理端(Vben5) | `account/login` → `account/Vue3Router/vben5` |
+| Token 混用 | 禁止:公司 Token 调平台路由,或平台 Token 调 `my-menus`(缺租户会失败) |
+
+---
+
+## 六、相关代码位置
+
+| 角色 | 路径 |
+|------|------|
+| 公司级菜单 | `food-labeling-us/.../AuthSessionAppService.cs` → `GetMyMenusAsync` |
+| 公司级登录 | `FoodLabeling.Th.Application/.../ThWebAuthAppService.cs` |
+| 平台级路由 | `Yi.Framework.Rbac.Application/.../AccountService.cs` → `GetVue3Router` |
+| 平台级登录 | 同 `AccountService` 登录接口 |
+
+---
+
+## 七、自检清单
+
+- [ ] 公司端:用 `th-web-auth/login` 拿到 Token 后再调 `auth-session/my-menus`
+- [ ] 公司端:Header 含 `__tenant` 或 JWT 内 `TenantId` 有效
+- [ ] 平台端:用 `account/login` 后再调 `account/Vue3Router/vben5`
+- [ ] 未把 `company-menus` / `menu-permission-tree` 误当作当前登录人菜单
+- [ ] Swagger 中确认实际 Host / 路径与本文一致
diff --git a/项目相关文档/2026-07-23美国版在线批量导入接口.md b/项目相关文档/2026-07-23美国版在线批量导入接口.md
new file mode 100644
index 0000000..312ab4e
--- /dev/null
+++ b/项目相关文档/2026-07-23美国版在线批量导入接口.md
@@ -0,0 +1,276 @@
+# 2026-07-23 美国版:Region / Location / Team Member / Product 在线批量导入
+
+本文档说明 **美国版**(`food-labeling-us`)四个 **JSON 在线批量导入**接口:前端在系统内表格/表单编辑后一次提交,**不再依赖下载 Excel 模板**(Excel 导入接口仍保留,但不推荐)。
+
+> 汇总表与 Excel/导出/批量编辑说明仍见:`项目相关文档/批量导入导出接口说明.md`。
+
+---
+
+## 一、为什么改成在线导入
+
+| 方式 | 问题 / 优势 |
+|------|-------------|
+| Excel 下载模板再上传 | 表头别名、列顺序、Region 名称 vs Id 等易填错;模板版本与环境不一致 |
+| **JSON 在线导入(本文)** | 与单条 `Create` 字段一致;前端下拉选 Company/Region/Location(传 Id);校验即时、失败行可定位 |
+
+---
+
+## 二、公共约定
+
+| 项 | 说明 |
+|----|------|
+| 宿主 | 美国版 `Yi.Abp.Web`;Swagger 分组「食品标签-美国版接口」 |
+| 基址示例 | `http://localhost:19001` / 测试环境 `https://flus-test.3ffoodsafety.com` |
+| Content-Type | `application/json` |
+| 鉴权 | `Authorization: Bearer {token}`(与其它业务接口相同) |
+| 请求体 | `{ "items": [ ... ] }`,每元素字段与对应模块**单条新增**一致 |
+| 行语义 | **仅新增**;批量编辑仍用各模块 `*-bulk`(Update) |
+| 失败策略 | **逐行**调用现有 `CreateAsync`;部分成功;单行 `UserFriendlyException` 记入 `errors` |
+| `index` | 失败行在 `items` 中的下标,**从 0 开始** |
+| 上限 | `FoodLabeling:BatchImport:MaxImportRows`(默认 **5000**) |
+| Excel 旧接口 | 保留;文档标注不推荐,请优先本文接口 |
+
+### 2.1 接口一览
+
+| 模块 | UI 概念 | HTTP | 路径 |
+|------|---------|------|------|
+| Group | **Region** | POST | `/api/app/group/batch-import-online` |
+| Location | Location | POST | `/api/app/location/batch-import-online` |
+| Team Member | Team Member | POST | `/api/app/team-member/batch-import-online` |
+| Product | Product | POST | `/api/app/product/batch-import-online` |
+
+### 2.2 统一出参结构
+
+```json
+{
+ "successCount": 2,
+ "failCount": 1,
+ "errors": [
+ {
+ "index": 2,
+ "message": "具体业务错误信息",
+ "groupName": "可选,按模块不同字段名见下表"
+ }
+ ]
+}
+```
+
+| 模块 | 失败行业务键字段(camelCase) |
+|------|------------------------------|
+| Region | `groupName` |
+| Location | `locationCode` |
+| Team Member | `userName` |
+| Product | `productName` |
+
+整单失败(HTTP 400):`items` 为空,或条数超过 `MaxImportRows`。
+
+### 2.3 与批量编辑的区别
+
+| | 在线批量导入(本文) | 批量编辑(已有) |
+|--|---------------------|------------------|
+| 路径示例 | `.../batch-import-online` | `.../locations-bulk` 等 |
+| 行语义 | 新建 | 按 `id` 更新 |
+| 典型场景 | 系统内批量录入新数据 | 网格「保存全部」 |
+
+---
+
+## 三、Region(Group)在线导入
+
+| 项 | 值 |
+|----|------|
+| 路径 | `POST /api/app/group/batch-import-online` |
+| 服务 | `GroupAppService.BatchImportOnlineAsync` |
+| items 元素 | `GroupCreateInputVo` |
+
+**字段映射**:UI **Region** = 库表 `fl_group.GroupName`(入参字段名仍为 `groupName`)。
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `groupName` | 是 | Region 名称 |
+| `partnerId` | 是 | 所属 Company(`fl_partner.Id`) |
+| `state` | 否 | 启用状态,默认 `true` |
+
+```bash
+curl -X POST "http://localhost:19001/api/app/group/batch-import-online" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"items\": [
+ { \"groupName\": \"NC Region\", \"partnerId\": \"\", \"state\": true },
+ { \"groupName\": \"SC Region\", \"partnerId\": \"\", \"state\": true }
+ ]
+ }"
+```
+
+权限与单条 `POST /api/app/group` 相同(含 Company Admin 自动绑定门店等逻辑)。
+
+---
+
+## 四、Location 在线导入
+
+| 项 | 值 |
+|----|------|
+| 路径 | `POST /api/app/location/batch-import-online` |
+| 服务 | `LocationAppService.BatchImportOnlineAsync` |
+| items 元素 | `LocationCreateInputVo` |
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `locationCode` | 是 | Location ID(业务编码) |
+| `locationName` | 是 | 门店名称 |
+| `partner` | 否 | Company **名称**(与单条新增一致,非 Id) |
+| `groupName` | 否 | Region 名称(对应 `location.GroupName`) |
+| `street` / `city` / `stateCode` / `country` / `zipCode` | 否 | 地址 |
+| `phone` / `email` | 否 | 联系方式 |
+| `latitude` / `longitude` | 否 | 坐标 |
+| `operatingHours` | 否 | 营业时间文本 |
+| `state` | 否 | 启用,默认 `true` |
+
+```bash
+curl -X POST "http://localhost:19001/api/app/location/batch-import-online" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"items\": [
+ {
+ \"locationCode\": \"LOC001\",
+ \"locationName\": \"UNCC store\",
+ \"partner\": \"MedVantage Cafe Group\",
+ \"groupName\": \"NC Region\",
+ \"city\": \"Charlotte\",
+ \"stateCode\": \"NC\",
+ \"country\": \"USA\",
+ \"state\": true
+ }
+ ]
+ }"
+```
+
+---
+
+## 五、Team Member 在线导入
+
+| 项 | 值 |
+|----|------|
+| 路径 | `POST /api/app/team-member/batch-import-online` |
+| 服务 | `TeamMemberAppService.BatchImportOnlineAsync` |
+| items 元素 | `TeamMemberCreateInputVo` |
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `fullName` | 是 | 姓名 |
+| `userName` | 是 | 登录账号 |
+| `password` | 否 | 空则用配置 `TeamMemberImportDefaultPassword` |
+| `email` / `phone` | 否 | 联系方式 |
+| `roleId` | 否 | 角色 Guid |
+| `partnerId` / `partnerIds` | 否 | Company Id |
+| `regionIds` / `groupIds` | 否 | Region(`fl_group.Id`),**传 Id 不要传名称** |
+| `locationIds` | 否 | 门店 Id 列表 |
+| `state` | 否 | 默认 `true` |
+
+与 Excel 导入差异:在线接口的 Region/Location 使用 **Id**,由前端下拉选择,避免名称拼写错误。
+
+```bash
+curl -X POST "http://localhost:19001/api/app/team-member/batch-import-online" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"items\": [
+ {
+ \"fullName\": \"John Doe\",
+ \"userName\": \"john@example.com\",
+ \"email\": \"john@example.com\",
+ \"roleId\": \"\",
+ \"locationIds\": [\"\"],
+ \"state\": true
+ }
+ ]
+ }"
+```
+
+---
+
+## 六、Product 在线导入
+
+| 项 | 值 |
+|----|------|
+| 路径 | `POST /api/app/product/batch-import-online` |
+| 服务 | `ProductAppService.BatchImportOnlineAsync` |
+| items 元素 | `ProductCreateInputVo` |
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `productName` | 是 | 产品名称 |
+| `productCode` | 否 | 空则后端生成唯一编码 |
+| `categoryId` | 否 | 分类 Id |
+| `productImageUrl` / `displayText` / `codeValue` | 否 | 展示相关 |
+| `buttonAppearance` / `categoryPhotoUrl` | 否 | 按钮外观 |
+| `availabilityType` | 否 | `ALL` / `SPECIFIED` |
+| `partnerId` | 否 | Company Id,展开门店后写入关联 |
+| `groupIds` | 否 | Region Id 列表 |
+| `locationIds` | 否 | 门店 Id 列表 |
+| `state` | 否 | 默认 `true` |
+
+```bash
+curl -X POST "http://localhost:19001/api/app/product/batch-import-online" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"items\": [
+ {
+ \"productName\": \"Tuna & Bacon Sub\",
+ \"productCode\": \"40001\",
+ \"categoryId\": \"\",
+ \"state\": true,
+ \"locationIds\": [\"\"]
+ }
+ ]
+ }"
+```
+
+---
+
+## 七、配置
+
+`appsettings` 节:`FoodLabeling:BatchImport`
+
+| 配置项 | 与在线导入相关 |
+|--------|----------------|
+| `MaxImportRows` | 单次 `items` 最大条数(默认 5000) |
+| `TeamMemberImportDefaultPassword` | Team Member 未填密码时的默认初始密码 |
+
+---
+
+## 八、前端对接建议
+
+1. 批量录入页用系统内表格 + 下拉(Company / Region / Location / Role),提交 JSON,**不要**再引导用户下载 Excel 模板。
+2. 展示返回的 `successCount` / `failCount`,按 `errors[].index` 高亮失败行。
+3. Region 展示文案用「Region」,请求字段仍用 `groupName` / `partnerId`。
+4. 批量改已有数据继续走各模块 **bulk update**,勿用本导入接口。
+5. 部署后需重启后端;Swagger 中确认路径为带模块前缀的 `.../group|location|team-member|product/batch-import-online`。
+
+---
+
+## 九、相关代码
+
+| 类型 | 路径 |
+|------|------|
+| Group | `FoodLabeling.Application/Services/GroupAppService.cs` |
+| Location | `FoodLabeling.Application/Services/LocationAppService.cs` |
+| Team Member | `FoodLabeling.Application/Services/TeamMemberAppService.cs` |
+| Product | `FoodLabeling.Application/Services/ProductAppService.cs` |
+| DTO | `FoodLabeling.Application.Contracts/Dtos/{Group,Location,TeamMember,Product}/*BatchImportOnline*.cs` |
+
+模块根目录:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/`。
+
+---
+
+## 十、自检清单
+
+- [ ] 平台/公司账号登录后 Token 可调业务接口
+- [ ] Region:合法 `partnerId` + `groupName` 可批量成功
+- [ ] Location:重复 `locationCode` 记入 `errors`,其它行仍成功
+- [ ] Team Member:不传 `password` 时使用默认密码可登录
+- [ ] Product:不传 `productCode` 时后端自动生成
+- [ ] `items` 超过 `MaxImportRows` 返回 400
+- [ ] 未误用 Excel 导入作为新前端主路径
diff --git a/项目相关文档/2026-07-24代码优化.md b/项目相关文档/2026-07-24代码优化.md
new file mode 100644
index 0000000..6d4d8fc
--- /dev/null
+++ b/项目相关文档/2026-07-24代码优化.md
@@ -0,0 +1,328 @@
+# 2026-07-24 Team Member 范围 ALL 哨兵(美国版后端)
+
+## 背景
+
+New User / Edit User 弹窗中 Locations、Region 支持勾选 **ALL**。前端可传 `locationIds: ["ALL"]`(或 `regionIds: ["ALL"]`),后端需按当前 Company(`partnerId` / `partnerIds`)展开并落库到 `userlocation`;列表 **Assigned Locations** 覆盖全部门店时展示 **`All Location`**,Region 覆盖全部时展示 **`All Region`**。
+
+本次**仅改美国版后端**,不改 Web 前端。
+
+---
+
+## 接口
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/api/app/team-member` | 新增成员 |
+| PUT | `/api/app/team-member/{id}` | 更新成员 |
+
+批量编辑 `PUT /api/app/team-member/team-members-bulk` 单行字段与单条 PUT 相同,同样支持 ALL。
+
+---
+
+## ALL 传参约定
+
+### 哨兵值
+
+- 字符串 **`ALL`**(大小写不敏感:`ALL` / `all` / `All`)
+- 适用字段:`locationIds`、`regionIds`、`groupIds`(与 `regionIds` 等价)、`locations`(与 `locationIds` 合并解析)
+
+### 与具体 Guid 同传
+
+若数组中同时含 `ALL` 与具体门店/Region Guid,**以 ALL 为准**(视为全选,忽略具体 Id)。
+
+### 优先级(Team Member 落库)
+
+**不再做 partner + region + location 并集**;按下列顺序取**单一**规则展开:
+
+1. **`locationIds` 含 `ALL`** → 按 `partnerId` / `partnerIds` 展开该公司**全部门店**
+2. **有具体 `locationIds`,且 `regionIds` 为空或为 `ALL`** → **只绑这些门店**(Region=ALL 下再收窄到指定门店;避免 `regionIds:["ALL"]` 盖掉单店)
+3. **`regionIds` 含 `ALL`**(且无具体门店)→ 按 Company 展开该公司**全部 Region** 下门店
+4. **`regionIds` 有具体 Guid** → **按这些 Region 展开门店落库**(**忽略同传的 `locationIds`**,保证多区域能落库回显)
+5. **仅有具体 `locationIds`** → 只绑定指定门店
+6. **仅有 `partnerId` / `partnerIds`**(Company Admin 等)→ 绑定该公司全部门店
+
+| 请求示例 | 落库规则 |
+|----------|----------|
+| `partnerId` + `locationIds: ["ALL"]` | 展开该公司**全部**门店写入 `userlocation` |
+| `partnerId` + `regionIds: ["ALL"]` + `locationIds: ["{loc}"]` | **只绑 `{loc}`**(Region ALL + 具体门店时门店优先) |
+| `partnerId` + `regionIds: ["ALL"]`(无 location) | 展开该公司**全部 Region** 下门店 |
+| `partnerId` + `regionIds: ["{groupGuid1}", "{groupGuid2}"]` | 展开两 Region 下门店并集 |
+| `partnerId` + `regionIds: ["{g1}","{g2}"]` + `locationIds: ["{loc}"]` | **按 Region 展开**(忽略 `{loc}`),`userlocation` 为两 Region 门店并集;Get 回显 `regionIds` 含 2 个 |
+| `partnerId` + `locationIds: ["{guid1}", "{guid2}"]`(无 region) | 仅绑定指定门店 |
+| Company Admin 仅传 `partnerId`(无 region/location) | 仍自动绑定该公司全部门店(原逻辑) |
+
+**注意**:`locationIds` / `regionIds` 含 ALL 时须传 **`partnerId` 或 `partnerIds`**,否则返回 400。
+
+### 编辑回显(GET 折叠为 ALL)
+
+库表 `userlocation` **仍存展开后的门店 Guid**。`GET /api/app/team-member/{id}`(及列表同名字段)在判断为「已覆盖该公司全部」时,将 Id 数组折叠为哨兵,与新增传 ALL 对称:
+
+| 条件 | 出参 |
+|------|------|
+| 绑定门店 = 该公司全部门店 | `locationIds: ["ALL"]`,`assignedLocations` 展示 All Location |
+| 反推 Region = 该公司全部 Region | `regionIds` / `groupIds: ["ALL"]` |
+| 未全选 | 仍返回具体 Guid 数组 |
+
+示例(新增时传 `regionIds/locationIds: ["ALL"]` 后,再 GET):
+
+```json
+{
+ "partnerIds": ["3a22a48c-9758-9a26-e4ee-390b0f042835"],
+ "regionIds": ["ALL"],
+ "groupIds": ["ALL"],
+ "locationIds": ["ALL"]
+}
+```
+
+---
+
+## 请求 / 响应示例
+
+### 1. Locations 全选(ALL)
+
+**请求**
+
+```json
+POST /api/app/team-member
+{
+ "fullName": "Test All Loc",
+ "userName": "test.all.loc@example.com",
+ "password": "Pass123!",
+ "email": "test.all.loc@example.com",
+ "roleId": "ROLE_GUID",
+ "partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e",
+ "locationIds": ["ALL"],
+ "state": true
+}
+```
+
+**落库**:`userlocation` 写入 Partner `Subway USA` 下全部未删除门店 Id。
+
+**列表/详情 `assignedLocations`**(覆盖全部门店时):
+
+```json
+"assignedLocations": [
+ { "locationName": "All Location" }
+]
+```
+
+### 2. 仅指定门店 Guid(对比)
+
+**请求**
+
+```json
+"partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e",
+"locationIds": [
+ "3a222878-b4c1-d27b-d437-93da14886350",
+ "3a22287c-d353-da47-869f-0bb5dacced51"
+]
+```
+
+**列表 `assignedLocations`**:展示具体名称,如 `Subway LAX Store`、`Subway UNCC Store`,**不会**出现 `All Location`。
+
+### 3. Region 全选(ALL)
+
+**请求**
+
+```json
+"partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e",
+"regionIds": ["ALL"]
+```
+
+**落库**:该公司下所有 Region 对应门店写入 `userlocation`。
+
+**Region 展示**:绑定覆盖该公司全部门店时,列表 Region 文案为 **`All Region`**(`FoodLabelingDisplayConsts.AllRegion`)。
+
+### 4. 编辑多 Region + 同传 location(按 Region 展开)
+
+**场景**:UI 勾选 2 个 Region,请求体可能仍带 1 个 `locationIds`(历史选中项)。后端**以 Region 为准**展开,保证多区域能落库并回显。
+
+**请求**
+
+```json
+PUT /api/app/team-member/{id}
+{
+ "fullName": "张三",
+ "userName": "123",
+ "email": "123@qq.com",
+ "roleId": "3a21836a-cc04-1783-b054-f2ff3c45d4bf",
+ "partnerId": "3a22a48c-9758-9a26-e4ee-390b0f042835",
+ "regionIds": [
+ "3a22a48d-98be-7d54-7b9d-6062afb176be",
+ "3a22a48d-5356-8754-d839-a6b7aaad02f9"
+ ],
+ "groupIds": [
+ "3a22a48d-98be-7d54-7b9d-6062afb176be",
+ "3a22a48d-5356-8754-d839-a6b7aaad02f9"
+ ],
+ "locationIds": ["3a22a48f-4114-ad64-11bf-9adb09f999b9"],
+ "state": true
+}
+```
+
+**落库**:`userlocation` 写入两 Region 下门店并集(**不是**仅 `{locationIds}` 那一店)。
+
+**Get 回显**:`regionIds` 含上述 2 个 Region(由 `userlocation` → 门店 → `fl_group` 反推)。
+
+---
+
+## 列表展示规则
+
+| 条件 | Assigned Locations | Region(列表/PDF) |
+|------|-------------------|-------------------|
+| `userlocation` 覆盖该成员 Company 下**全部门店** | `All Location` | 全部 Region 时 `All Region` |
+| 仅部分门店 | 门店 Code/Name,分号拼接 | 具体 Region 名称 |
+
+实现位置:
+
+- 落库展开:`LocationScopeBindingHelper.ResolveTeamMemberLocationIdsForSaveAsync`
+- 展示折叠:`TeamMemberScopeDisplayHelper.FormatAssignedLocationsForListAsync` / `FormatRegionTextForListAsync`
+- 全量回显:`TeamMemberAppService.ApplyCompanyAdminDisplayScopeAsync`(绑定覆盖全部门店时展开 Region)
+
+---
+
+## curl 示例
+
+### 获取 Token
+
+```bash
+curl -X POST "http://localhost:19001/api/oauth/Login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "userName=admin&password=123456"
+```
+
+将响应中 `data.token`(已含 `Bearer ` 前缀)用于后续 `Authorization` 头。
+
+### 新增 — Locations ALL
+
+```bash
+curl -X POST "http://localhost:19001/api/app/team-member" \
+ -H "Authorization: " \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"fullName\": \"All Loc User\",
+ \"userName\": \"all.loc.user@example.com\",
+ \"password\": \"Pass123!\",
+ \"email\": \"all.loc.user@example.com\",
+ \"roleId\": \"\",
+ \"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\",
+ \"locationIds\": [\"ALL\"],
+ \"state\": true
+ }"
+```
+
+### 更新 — Region ALL
+
+```bash
+curl -X PUT "http://localhost:19001/api/app/team-member/" \
+ -H "Authorization: " \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"fullName\": \"All Region User\",
+ \"userName\": \"all.region.user@example.com\",
+ \"roleId\": \"\",
+ \"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\",
+ \"regionIds\": [\"ALL\"],
+ \"state\": true
+ }"
+```
+
+### 查询列表验证 Assigned Locations
+
+```bash
+curl -X GET "http://localhost:19001/api/app/team-member?SkipCount=0&MaxResultCount=20" \
+ -H "Authorization: "
+```
+
+检查 `items[].assignedLocations` 与 `items[].locationIds`。
+
+---
+
+## 改动文件
+
+| 文件 | 变更 |
+|------|------|
+| `FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs` | ALL 哨兵识别;`ResolveTeamMemberLocationIdsForSaveAsync` 展开逻辑 |
+| `FoodLabeling.Application/Services/TeamMemberAppService.cs` | 合并 `locations`;Create/Update XML 注释;全量绑定展示 |
+| `FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs` | 字段注释 + `Locations` |
+| `FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs` | 字段注释 + `Locations` |
+| `FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs` | Create/Update 接口说明 |
+
+---
+
+## 验证
+
+- `dotnet build` 编译通过
+- 可选:MCP `user-antis-foodlabeling-us-mysql` 查 `userlocation` 确认 ALL 请求后行数等于该公司 `location` 未删除门店数
+
+---
+
+## label-template 多公司 / 多 Region / 多门店传参与落库(美国版)
+
+### 背景
+
+`POST/PUT /api/app/label-template` 在 **Company / Region / Location** 三维均为 `SPECIFIED` 且各传多个 Id 时,若所选 Id 恰好等于「当前上下文全部可选项」,旧逻辑会把维度折叠为 `ALL` 并**清空关联表**,详情再按门店反推 Company,常表现为 **只回显 1 个公司**。
+
+### 根因
+
+`AllScopeBindingHelper.ResolveDimensionType` 在 `isFullSelection == true` 时一律返回 `ALL`,**未尊重入参显式 `appliedPartnerType` / `appliedRegionType` / `appliedLocation` = `SPECIFIED`**。
+
+### 修复约定
+
+| 入参 | 落库行为 |
+|------|----------|
+| `appliedPartnerType: "SPECIFIED"` + `partnerIds` / `companyIds` 数组 | **始终**写 `fl_label_template_partner`(每个 Id 一行),主表 `AppliedPartnerType = SPECIFIED` |
+| `appliedRegionType: "SPECIFIED"` + `regionIds` / `groupIds` | **始终**写 `fl_label_template_region` |
+| `appliedLocation: "SPECIFIED"` + `locationIds` / `appliedLocationIds` | **始终**写 `fl_label_template_location` |
+| 各维度 `ALL` 或未显式 `SPECIFIED` 且 Id 覆盖当前上下文全集 | 仍归档为 `ALL`,不写该维度关联快照(后续新增主数据动态适用) |
+
+**字段别名(合并去重)**
+
+- Company:`partnerIds` ≡ `companyIds`(`fl_partner.Id`)
+- Region:`regionIds` ≡ `groupIds`(`fl_group.Id`)
+- Location:`locationIds` ≡ `appliedLocationIds`(`location.Id`)
+
+### 请求示例(2 个 Company + 2 个 Region + 2 个门店)
+
+```json
+PUT /api/app/label-template/tpl_xxx
+{
+ "appliedPartnerType": "SPECIFIED",
+ "companyIds": ["{partnerId1}", "{partnerId2}"],
+ "partnerIds": ["{partnerId1}", "{partnerId2}"],
+ "appliedRegionType": "SPECIFIED",
+ "regionIds": ["{groupId1}", "{groupId2}"],
+ "groupIds": ["{groupId1}", "{groupId2}"],
+ "appliedLocation": "SPECIFIED",
+ "locationIds": ["{locId1}", "{locId2}"],
+ "appliedLocationIds": ["{locId1}", "{locId2}"]
+}
+```
+
+### 验证(MCP / curl)
+
+1. 保存后查库:
+
+```sql
+SELECT AppliedPartnerType, AppliedRegionType, AppliedLocationType
+FROM fl_label_template WHERE TemplateCode = 'tpl_xxx';
+
+SELECT PartnerId FROM fl_label_template_partner
+WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx');
+
+SELECT GroupId FROM fl_label_template_region
+WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx');
+
+SELECT LocationId FROM fl_label_template_location
+WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx');
+```
+
+2. `GET /api/app/label-template/tpl_xxx` 出参 `companyIds` / `partnerIds` / `regionIds` / `locationIds` 数组长度应与入参一致。
+
+### 改动文件
+
+| 文件 | 变更 |
+|------|------|
+| `Helpers/AllScopeBindingHelper.cs` | 显式 `SPECIFIED` 时不因全选折叠为 `ALL` |
+| `Helpers/LabelTemplateScopeHelper.cs` | 保存传参对齐 Label 实体;详情/列表读取主表 scope 类型,避免门店反推覆盖 |
+| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 批量读取 `AppliedPartnerType` / `AppliedRegionType` |
diff --git a/项目相关文档/th-platform-user-role-seed.sql b/项目相关文档/th-platform-user-role-seed.sql
new file mode 100644
index 0000000..6d7761f
--- /dev/null
+++ b/项目相关文档/th-platform-user-role-seed.sql
@@ -0,0 +1,159 @@
+-- 泰额版平台主库 User / Role / UserRole / RoleMenu 初始化
+-- 执行库(必须):antis-foodlabeling-host
+-- 禁止执行库:antis-foodlabeling-us(美国版,写入会污染)
+-- 前置:已执行 th-tenant-menu-seed.sql(主库已有 menu)
+-- 默认账号:admin / 123456(密码哈希与框架种子一致)
+--
+-- ========== 固定 Guid 对照表 ==========
+-- 平台超级管理员角色 f0020002-0001-4000-8000-000000000002
+-- 平台 admin 用户 f0020001-0001-4000-8000-000000000001
+-- 用户-角色关系 f0020003-0001-4000-8000-000000000003
+-- 角色-菜单关系 f0030001~f0030026(见下方 INSERT)
+-- ======================================
+
+CREATE TABLE IF NOT EXISTS `user` (
+ `Id` varchar(36) NOT NULL COMMENT '主键',
+ `IsDeleted` tinyint(1) NOT NULL COMMENT '逻辑删除',
+ `Name` varchar(255) DEFAULT NULL COMMENT '姓名',
+ `Age` int(11) DEFAULT NULL COMMENT '年龄',
+ `UserName` varchar(255) NOT NULL COMMENT '用户名',
+ `Password` varchar(255) NOT NULL COMMENT '密码',
+ `Salt` varchar(255) NOT NULL COMMENT '加密盐值',
+ `Icon` varchar(255) DEFAULT NULL COMMENT '头像',
+ `Nick` varchar(255) DEFAULT NULL COMMENT '昵称',
+ `Email` varchar(255) DEFAULT NULL COMMENT '邮箱',
+ `Ip` varchar(255) DEFAULT NULL COMMENT 'Ip',
+ `Address` varchar(255) DEFAULT NULL COMMENT '地址',
+ `Phone` bigint(20) DEFAULT NULL COMMENT '电话',
+ `Introduction` varchar(255) DEFAULT NULL COMMENT '简介',
+ `Remark` varchar(255) DEFAULT NULL COMMENT '备注',
+ `Sex` int(11) NOT NULL COMMENT '性别',
+ `DeptId` varchar(36) DEFAULT NULL COMMENT '部门id',
+ `CreationTime` datetime NOT NULL COMMENT '创建时间',
+ `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者',
+ `LastModifierId` varchar(36) DEFAULT NULL COMMENT '最后修改者',
+ `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间',
+ `OrderNum` int(11) NOT NULL COMMENT '排序',
+ `State` tinyint(1) NOT NULL COMMENT '状态',
+ `ConcurrencyStamp` varchar(255) NOT NULL,
+ PRIMARY KEY (`Id`),
+ KEY `index_UserName` (`UserName`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表-泰额平台主库';
+
+CREATE TABLE IF NOT EXISTS `role` (
+ `Id` varchar(36) NOT NULL COMMENT '主键',
+ `IsDeleted` tinyint(1) NOT NULL COMMENT '逻辑删除',
+ `CreationTime` datetime NOT NULL COMMENT '创建时间',
+ `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者',
+ `LastModifierId` varchar(36) DEFAULT NULL COMMENT '最后修改者',
+ `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间',
+ `OrderNum` int(11) NOT NULL COMMENT '排序',
+ `RoleName` varchar(255) NOT NULL COMMENT '角色名',
+ `RoleCode` varchar(255) NOT NULL COMMENT '角色编码',
+ `Remark` varchar(255) DEFAULT NULL COMMENT '描述',
+ `DataScope` int(11) NOT NULL COMMENT '角色数据范围',
+ `State` tinyint(1) NOT NULL COMMENT '状态',
+ `AccessPermissionCodes` varchar(2000) DEFAULT NULL COMMENT '访问权限 JSON',
+ `ConcurrencyStamp` varchar(255) NOT NULL,
+ PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表-泰额平台主库';
+
+CREATE TABLE IF NOT EXISTS `userrole` (
+ `Id` varchar(36) NOT NULL COMMENT '主键',
+ `RoleId` varchar(36) NOT NULL COMMENT '角色id',
+ `UserId` varchar(36) NOT NULL COMMENT '用户id',
+ PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关系表-泰额平台主库';
+
+CREATE TABLE IF NOT EXISTS `rolemenu` (
+ `Id` varchar(36) NOT NULL COMMENT '主键',
+ `RoleId` varchar(36) NOT NULL,
+ `MenuId` varchar(36) NOT NULL,
+ PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关系表-泰额平台主库';
+
+-- 物理清除旧非法 Id,避免脏数据残留
+DELETE FROM `userrole` WHERE `Id` LIKE 'th-platform%' OR `UserId` LIKE 'th-platform%' OR `RoleId` LIKE 'th-platform%';
+DELETE FROM `rolemenu` WHERE `RoleId` LIKE 'th-platform%' OR `Id` LIKE 'th-prm-%' OR `Id` LIKE 'th-platform%';
+DELETE FROM `user` WHERE `Id` LIKE 'th-platform%';
+DELETE FROM `role` WHERE `Id` LIKE 'th-platform%';
+
+INSERT INTO `role`
+(`Id`, `IsDeleted`, `CreationTime`, `CreatorId`, `LastModifierId`, `LastModificationTime`,
+ `OrderNum`, `RoleName`, `RoleCode`, `Remark`, `DataScope`, `State`, `AccessPermissionCodes`, `ConcurrencyStamp`)
+VALUES
+('f0020002-0001-4000-8000-000000000002', 0, NOW(), NULL, NULL, NULL,
+ 999, '平台超级管理员', 'admin', '泰额平台主库超级管理员', 0, 1, NULL, UUID())
+ON DUPLICATE KEY UPDATE
+ `IsDeleted`=0,
+ `LastModificationTime`=NOW(),
+ `RoleName`=VALUES(`RoleName`),
+ `RoleCode`=VALUES(`RoleCode`),
+ `Remark`=VALUES(`Remark`),
+ `DataScope`=VALUES(`DataScope`),
+ `State`=1,
+ `ConcurrencyStamp`=UUID();
+
+INSERT INTO `user`
+(`Id`, `IsDeleted`, `Name`, `Age`, `UserName`, `Password`, `Salt`, `Icon`, `Nick`, `Email`,
+ `Ip`, `Address`, `Phone`, `Introduction`, `Remark`, `Sex`, `DeptId`,
+ `CreationTime`, `CreatorId`, `LastModifierId`, `LastModificationTime`,
+ `OrderNum`, `State`, `ConcurrencyStamp`)
+VALUES
+('f0020001-0001-4000-8000-000000000001', 0, '超级管理员', 20, 'admin',
+ 'ANg9hGZC3CMBZsfyTE8-AHmP7A30DrI85LxR1btI4HjNwk-wilA9y2-_L4y_kTS14Qq8uIVOeXdYggrShfvNsQ',
+ 'EMXTQOSLmpRmBGS4tMz4mw==',
+ NULL, '超级管理员', 'admin@example.com',
+ NULL, '成都', 13800000000, '泰额平台超级管理员', '平台主库', 0, NULL,
+ NOW(), NULL, NULL, NULL,
+ 999, 1, UUID())
+ON DUPLICATE KEY UPDATE
+ `IsDeleted`=0,
+ `LastModificationTime`=NOW(),
+ `Name`=VALUES(`Name`),
+ `Nick`=VALUES(`Nick`),
+ `Email`=VALUES(`Email`),
+ `Password`=VALUES(`Password`),
+ `Salt`=VALUES(`Salt`),
+ `State`=1,
+ `ConcurrencyStamp`=UUID();
+
+INSERT INTO `userrole` (`Id`, `RoleId`, `UserId`)
+VALUES ('f0020003-0001-4000-8000-000000000003', 'f0020002-0001-4000-8000-000000000002', 'f0020001-0001-4000-8000-000000000001')
+ON DUPLICATE KEY UPDATE
+ `RoleId`=VALUES(`RoleId`),
+ `UserId`=VALUES(`UserId`);
+
+DELETE FROM `rolemenu` WHERE `RoleId` = 'f0020002-0001-4000-8000-000000000002';
+
+INSERT INTO `rolemenu` (`Id`, `RoleId`, `MenuId`)
+VALUES
+('f0030001-0001-4000-8000-000000000001', 'f0020002-0001-4000-8000-000000000002', 'f0010001-0001-4000-8000-000000000001'),
+('f0030002-0001-4000-8000-000000000002', 'f0020002-0001-4000-8000-000000000002', 'f0010002-0001-4000-8000-000000000002'),
+('f0030003-0001-4000-8000-000000000003', 'f0020002-0001-4000-8000-000000000002', 'f0010003-0001-4000-8000-000000000003'),
+('f0030004-0001-4000-8000-000000000004', 'f0020002-0001-4000-8000-000000000002', 'f0010010-0001-4000-8000-000000000010'),
+('f0030005-0001-4000-8000-000000000005', 'f0020002-0001-4000-8000-000000000002', 'f0010011-0001-4000-8000-000000000011'),
+('f0030006-0001-4000-8000-000000000006', 'f0020002-0001-4000-8000-000000000002', 'f0010012-0001-4000-8000-000000000012'),
+('f0030007-0001-4000-8000-000000000007', 'f0020002-0001-4000-8000-000000000002', 'f0010013-0001-4000-8000-000000000013'),
+('f0030008-0001-4000-8000-000000000008', 'f0020002-0001-4000-8000-000000000002', 'f0010014-0001-4000-8000-000000000014'),
+('f0030009-0001-4000-8000-000000000009', 'f0020002-0001-4000-8000-000000000002', 'f0010015-0001-4000-8000-000000000015'),
+('f0030010-0001-4000-8000-000000000010', 'f0020002-0001-4000-8000-000000000002', 'f0010020-0001-4000-8000-000000000020'),
+('f0030011-0001-4000-8000-000000000011', 'f0020002-0001-4000-8000-000000000002', 'f0010021-0001-4000-8000-000000000021'),
+('f0030012-0001-4000-8000-000000000012', 'f0020002-0001-4000-8000-000000000002', 'f0010022-0001-4000-8000-000000000022'),
+('f0030013-0001-4000-8000-000000000013', 'f0020002-0001-4000-8000-000000000002', 'f0010023-0001-4000-8000-000000000023'),
+('f0030014-0001-4000-8000-000000000014', 'f0020002-0001-4000-8000-000000000002', 'f0010024-0001-4000-8000-000000000024'),
+('f0030015-0001-4000-8000-000000000015', 'f0020002-0001-4000-8000-000000000002', 'f0010025-0001-4000-8000-000000000025'),
+('f0030016-0001-4000-8000-000000000016', 'f0020002-0001-4000-8000-000000000002', 'f0010026-0001-4000-8000-000000000026'),
+('f0030017-0001-4000-8000-000000000017', 'f0020002-0001-4000-8000-000000000002', 'f0010030-0001-4000-8000-000000000030'),
+('f0030018-0001-4000-8000-000000000018', 'f0020002-0001-4000-8000-000000000002', 'f0010031-0001-4000-8000-000000000031'),
+('f0030019-0001-4000-8000-000000000019', 'f0020002-0001-4000-8000-000000000002', 'f0010032-0001-4000-8000-000000000032'),
+('f0030020-0001-4000-8000-000000000020', 'f0020002-0001-4000-8000-000000000002', 'f0010033-0001-4000-8000-000000000033'),
+('f0030021-0001-4000-8000-000000000021', 'f0020002-0001-4000-8000-000000000002', 'f0010034-0001-4000-8000-000000000034'),
+('f0030022-0001-4000-8000-000000000022', 'f0020002-0001-4000-8000-000000000002', 'f0010035-0001-4000-8000-000000000035'),
+('f0030023-0001-4000-8000-000000000023', 'f0020002-0001-4000-8000-000000000002', 'f0010036-0001-4000-8000-000000000036'),
+('f0030024-0001-4000-8000-000000000024', 'f0020002-0001-4000-8000-000000000002', 'f0010037-0001-4000-8000-000000000037'),
+('f0030025-0001-4000-8000-000000000025', 'f0020002-0001-4000-8000-000000000002', 'f0010038-0001-4000-8000-000000000038'),
+('f0030026-0001-4000-8000-000000000026', 'f0020002-0001-4000-8000-000000000002', 'f0010039-0001-4000-8000-000000000039')
+ON DUPLICATE KEY UPDATE
+ `RoleId` = VALUES(`RoleId`),
+ `MenuId` = VALUES(`MenuId`);
diff --git a/项目相关文档/th-tenant-menu-seed.sql b/项目相关文档/th-tenant-menu-seed.sql
new file mode 100644
index 0000000..c19492d
--- /dev/null
+++ b/项目相关文档/th-tenant-menu-seed.sql
@@ -0,0 +1,191 @@
+-- 泰额版 menu 菜单初始化脚本(平台主库 / 租户业务库通用)
+-- 执行库示例:antis-foodlabeling-host(平台主库);租户业务库按需切换
+-- 禁止对 antis-foodlabeling-us 执行(美国版)
+--
+-- MenuType: 0=目录, 1=菜单, 2=组件/按钮
+-- MenuSource: 0=Ruoyi, 1=Pure, 2=Vben5;当前固定 Ruoyi(0)
+-- 根菜单 ParentId 用 '0';子菜单 ParentId 用父菜单 Guid
+--
+-- ========== 固定 Guid 对照表(名称 → Id) ==========
+-- 首页概览 f0010001-0001-4000-8000-000000000001
+-- 平台管理 f0010002-0001-4000-8000-000000000002
+-- SAAS 公司 f0010003-0001-4000-8000-000000000003
+-- 标签管理 f0010010-0001-4000-8000-000000000010
+-- 标签 f0010011-0001-4000-8000-000000000011
+-- 标签分类 f0010012-0001-4000-8000-000000000012
+-- 标签类型 f0010013-0001-4000-8000-000000000013
+-- 标签模板 f0010014-0001-4000-8000-000000000014
+-- 多选选项集 f0010015-0001-4000-8000-000000000015
+-- 业务模块 f0010020-0001-4000-8000-000000000020
+-- 培训 f0010021-0001-4000-8000-000000000021
+-- 告警 f0010022-0001-4000-8000-000000000022
+-- 任务 f0010023-0001-4000-8000-000000000023
+-- 传感器 f0010024-0001-4000-8000-000000000024
+-- 食物浪费 f0010025-0001-4000-8000-000000000025
+-- 电子标签 f0010026-0001-4000-8000-000000000026
+-- 管理 f0010030-0001-4000-8000-000000000030
+-- 账户管理 f0010031-0001-4000-8000-000000000031
+-- 系统菜单 f0010032-0001-4000-8000-000000000032
+-- 菜单管理 f0010033-0001-4000-8000-000000000033
+-- 设备 f0010034-0001-4000-8000-000000000034
+-- 报表 f0010035-0001-4000-8000-000000000035
+-- 发票 f0010036-0001-4000-8000-000000000036
+-- 二维码 f0010037-0001-4000-8000-000000000037
+-- 支持 f0010038-0001-4000-8000-000000000038
+-- API f0010039-0001-4000-8000-000000000039
+-- ==================================================
+
+-- 1a) 物理删除非法 Id(th-menu-*),避免留下脏数据行
+DELETE FROM `rolemenu` WHERE `MenuId` LIKE 'th-menu-%';
+DELETE FROM `menu` WHERE `Id` LIKE 'th-menu-%';
+
+-- 1b) 同 PermissionCode 的其它旧行(非本脚本固定 Guid)做逻辑删除
+UPDATE `menu`
+SET `IsDeleted` = 1,
+ `LastModificationTime` = NOW()
+WHERE `IsDeleted` = 0
+ AND (
+ (
+ `PermissionCode` IN (
+ 'menu.dashboard.analytics',
+ 'menu.platform',
+ 'menu.platform.tenants',
+ 'menu.labeling',
+ 'menu.labeling.labels',
+ 'menu.labeling.categories',
+ 'menu.labeling.types',
+ 'menu.labeling.templates',
+ 'menu.labeling.multiple-options',
+ 'menu.modules',
+ 'menu.modules.training',
+ 'menu.modules.alerts',
+ 'menu.modules.tasks',
+ 'menu.modules.sensors',
+ 'menu.modules.food-waste',
+ 'menu.modules.e-label',
+ 'menu.management',
+ 'menu.management.account',
+ 'menu.management.system-menu',
+ 'menu.management.menu',
+ 'menu.management.devices',
+ 'menu.management.reports',
+ 'menu.management.invoices',
+ 'menu.management.qr-codes',
+ 'menu.management.support',
+ 'menu.management.api'
+ )
+ AND `Id` NOT IN (
+ 'f0010001-0001-4000-8000-000000000001',
+ 'f0010002-0001-4000-8000-000000000002',
+ 'f0010003-0001-4000-8000-000000000003',
+ 'f0010010-0001-4000-8000-000000000010',
+ 'f0010011-0001-4000-8000-000000000011',
+ 'f0010012-0001-4000-8000-000000000012',
+ 'f0010013-0001-4000-8000-000000000013',
+ 'f0010014-0001-4000-8000-000000000014',
+ 'f0010015-0001-4000-8000-000000000015',
+ 'f0010020-0001-4000-8000-000000000020',
+ 'f0010021-0001-4000-8000-000000000021',
+ 'f0010022-0001-4000-8000-000000000022',
+ 'f0010023-0001-4000-8000-000000000023',
+ 'f0010024-0001-4000-8000-000000000024',
+ 'f0010025-0001-4000-8000-000000000025',
+ 'f0010026-0001-4000-8000-000000000026',
+ 'f0010030-0001-4000-8000-000000000030',
+ 'f0010031-0001-4000-8000-000000000031',
+ 'f0010032-0001-4000-8000-000000000032',
+ 'f0010033-0001-4000-8000-000000000033',
+ 'f0010034-0001-4000-8000-000000000034',
+ 'f0010035-0001-4000-8000-000000000035',
+ 'f0010036-0001-4000-8000-000000000036',
+ 'f0010037-0001-4000-8000-000000000037',
+ 'f0010038-0001-4000-8000-000000000038',
+ 'f0010039-0001-4000-8000-000000000039'
+ )
+ )
+ );
+
+-- 2) 插入 / 更新 26 条食品 SaaS 菜单(合法 Guid)
+INSERT INTO `menu`
+(
+ `Id`,
+ `IsDeleted`,
+ `CreationTime`,
+ `CreatorId`,
+ `LastModifierId`,
+ `LastModificationTime`,
+ `OrderNum`,
+ `State`,
+ `MenuName`,
+ `RouterName`,
+ `MenuType`,
+ `PermissionCode`,
+ `ParentId`,
+ `MenuIcon`,
+ `Router`,
+ `IsLink`,
+ `IsCache`,
+ `IsShow`,
+ `Remark`,
+ `Component`,
+ `MenuSource`,
+ `Query`,
+ `ConcurrencyStamp`
+)
+VALUES
+('f0010001-0001-4000-8000-000000000001', 0, NOW(), NULL, NULL, NULL, -1, 1, '首页概览', 'FoodLabelingDashboard', 1, 'menu.dashboard.analytics', '0', 'lucide:layout-dashboard', '/analytics', 0, 0, 1, NULL, '/food-labeling/dashboard/index', 0, NULL, UUID()),
+
+('f0010002-0001-4000-8000-000000000002', 0, NOW(), NULL, NULL, NULL, 5, 1, '平台管理', 'FoodLabelingPlatform', 0, 'menu.platform', '0', 'lucide:cloud-cog', '/platform', 0, 0, 1, NULL, NULL, 0, NULL, UUID()),
+('f0010003-0001-4000-8000-000000000003', 0, NOW(), NULL, NULL, NULL, 6, 1, 'SAAS 公司', 'FoodLabelingPlatformTenants', 1, 'menu.platform.tenants', 'f0010002-0001-4000-8000-000000000002', 'lucide:building', '/platform/tenants', 0, 0, 1, NULL, '/food-labeling/platform/tenants/index', 0, NULL, UUID()),
+
+('f0010010-0001-4000-8000-000000000010', 0, NOW(), NULL, NULL, NULL, 10, 1, '标签管理', 'FoodLabelingLabeling', 0, 'menu.labeling', '0', 'lucide:tags', '/labeling', 0, 0, 1, NULL, NULL, 0, NULL, UUID()),
+('f0010011-0001-4000-8000-000000000011', 0, NOW(), NULL, NULL, NULL, 11, 1, '标签', 'FoodLabelingLabels', 1, 'menu.labeling.labels', 'f0010010-0001-4000-8000-000000000010', 'lucide:tag', '/labels', 0, 0, 1, NULL, '/food-labeling/labeling/labels/index', 0, NULL, UUID()),
+('f0010012-0001-4000-8000-000000000012', 0, NOW(), NULL, NULL, NULL, 12, 1, '标签分类', 'FoodLabelingLabelCategories', 1, 'menu.labeling.categories', 'f0010010-0001-4000-8000-000000000010', 'lucide:folder-tree', '/label-categories', 0, 0, 1, NULL, '/food-labeling/labeling/label-categories/index', 0, NULL, UUID()),
+('f0010013-0001-4000-8000-000000000013', 0, NOW(), NULL, NULL, NULL, 13, 1, '标签类型', 'FoodLabelingLabelTypes', 1, 'menu.labeling.types', 'f0010010-0001-4000-8000-000000000010', 'lucide:layers', '/label-types', 0, 0, 1, NULL, '/food-labeling/labeling/label-types/index', 0, NULL, UUID()),
+('f0010014-0001-4000-8000-000000000014', 0, NOW(), NULL, NULL, NULL, 14, 1, '标签模板', 'FoodLabelingLabelTemplates', 1, 'menu.labeling.templates', 'f0010010-0001-4000-8000-000000000010', 'lucide:layout-template', '/label-templates', 0, 0, 1, NULL, '/food-labeling/labeling/label-templates/index', 0, NULL, UUID()),
+('f0010015-0001-4000-8000-000000000015', 0, NOW(), NULL, NULL, NULL, 15, 1, '多选选项集', 'FoodLabelingMultipleOptions', 1, 'menu.labeling.multiple-options', 'f0010010-0001-4000-8000-000000000010', 'lucide:list-checks', '/multiple-options', 0, 0, 1, NULL, '/food-labeling/labeling/multiple-options/index', 0, NULL, UUID()),
+
+('f0010020-0001-4000-8000-000000000020', 0, NOW(), NULL, NULL, NULL, 15, 1, '业务模块', 'FoodLabelingModules', 0, 'menu.modules', '0', 'lucide:boxes', '/modules', 0, 0, 1, NULL, NULL, 0, NULL, UUID()),
+('f0010021-0001-4000-8000-000000000021', 0, NOW(), NULL, NULL, NULL, 16, 1, '培训', 'FoodLabelingTraining', 1, 'menu.modules.training', 'f0010020-0001-4000-8000-000000000020', 'lucide:graduation-cap', '/training', 0, 0, 1, NULL, '/food-labeling/modules/training/index', 0, NULL, UUID()),
+('f0010022-0001-4000-8000-000000000022', 0, NOW(), NULL, NULL, NULL, 17, 1, '告警', 'FoodLabelingAlerts', 1, 'menu.modules.alerts', 'f0010020-0001-4000-8000-000000000020', 'lucide:bell', '/alerts', 0, 0, 1, NULL, '/food-labeling/modules/alerts/index', 0, NULL, UUID()),
+('f0010023-0001-4000-8000-000000000023', 0, NOW(), NULL, NULL, NULL, 18, 1, '任务', 'FoodLabelingTasks', 1, 'menu.modules.tasks', 'f0010020-0001-4000-8000-000000000020', 'lucide:list-todo', '/tasks', 0, 0, 1, NULL, '/food-labeling/modules/tasks/index', 0, NULL, UUID()),
+('f0010024-0001-4000-8000-000000000024', 0, NOW(), NULL, NULL, NULL, 19, 1, '传感器', 'FoodLabelingSensors', 1, 'menu.modules.sensors', 'f0010020-0001-4000-8000-000000000020', 'lucide:activity', '/sensors', 0, 0, 1, NULL, '/food-labeling/modules/sensors/index', 0, NULL, UUID()),
+('f0010025-0001-4000-8000-000000000025', 0, NOW(), NULL, NULL, NULL, 20, 1, '食物浪费', 'FoodLabelingFoodWaste', 1, 'menu.modules.food-waste', 'f0010020-0001-4000-8000-000000000020', 'lucide:apple', '/food-waste', 0, 0, 1, NULL, '/food-labeling/modules/food-waste/index', 0, NULL, UUID()),
+('f0010026-0001-4000-8000-000000000026', 0, NOW(), NULL, NULL, NULL, 21, 1, '电子标签', 'FoodLabelingELabelModule', 1, 'menu.modules.e-label', 'f0010020-0001-4000-8000-000000000020', 'lucide:file-digit', '/e-label-module', 0, 0, 1, NULL, '/food-labeling/modules/e-label/index', 0, NULL, UUID()),
+
+('f0010030-0001-4000-8000-000000000030', 0, NOW(), NULL, NULL, NULL, 20, 1, '管理', 'FoodLabelingManagement', 0, 'menu.management', '0', 'lucide:building-2', '/management', 0, 0, 1, NULL, NULL, 0, NULL, UUID()),
+('f0010031-0001-4000-8000-000000000031', 0, NOW(), NULL, NULL, NULL, 21, 1, '账户管理', 'FoodLabelingAccountManagement', 1, 'menu.management.account', 'f0010030-0001-4000-8000-000000000030', 'lucide:users', '/account-management', 0, 0, 1, NULL, '/food-labeling/management/account-management/index', 0, NULL, UUID()),
+('f0010032-0001-4000-8000-000000000032', 0, NOW(), NULL, NULL, NULL, 22, 1, '系统菜单', 'FoodLabelingSystemMenu', 1, 'menu.management.system-menu', 'f0010030-0001-4000-8000-000000000030', 'lucide:menu-square', '/system-menu', 0, 0, 1, NULL, '/food-labeling/management/system-menu/index', 0, NULL, UUID()),
+('f0010033-0001-4000-8000-000000000033', 0, NOW(), NULL, NULL, NULL, 23, 1, '菜单管理', 'FoodLabelingMenuManagement', 1, 'menu.management.menu', 'f0010030-0001-4000-8000-000000000030', 'lucide:utensils', '/menu-management', 0, 0, 1, NULL, '/food-labeling/management/menu-management/index', 0, NULL, UUID()),
+('f0010034-0001-4000-8000-000000000034', 0, NOW(), NULL, NULL, NULL, 24, 1, '设备', 'FoodLabelingDevices', 1, 'menu.management.devices', 'f0010030-0001-4000-8000-000000000030', 'lucide:smartphone', '/devices', 0, 0, 1, NULL, '/food-labeling/modules/devices/index', 0, NULL, UUID()),
+('f0010035-0001-4000-8000-000000000035', 0, NOW(), NULL, NULL, NULL, 25, 1, '报表', 'FoodLabelingReports', 1, 'menu.management.reports', 'f0010030-0001-4000-8000-000000000030', 'lucide:file-bar-chart', '/reports', 0, 0, 1, NULL, '/food-labeling/management/reports/index', 0, NULL, UUID()),
+('f0010036-0001-4000-8000-000000000036', 0, NOW(), NULL, NULL, NULL, 26, 1, '发票', 'FoodLabelingInvoices', 1, 'menu.management.invoices', 'f0010030-0001-4000-8000-000000000030', 'lucide:receipt', '/invoices', 0, 0, 1, NULL, '/food-labeling/modules/invoices/index', 0, NULL, UUID()),
+('f0010037-0001-4000-8000-000000000037', 0, NOW(), NULL, NULL, NULL, 27, 1, '二维码', 'FoodLabelingQrCodes', 1, 'menu.management.qr-codes', 'f0010030-0001-4000-8000-000000000030', 'lucide:qr-code', '/qr-codes', 0, 0, 1, NULL, '/food-labeling/modules/qr-codes/index', 0, NULL, UUID()),
+('f0010038-0001-4000-8000-000000000038', 0, NOW(), NULL, NULL, NULL, 28, 1, '支持', 'FoodLabelingSupport', 1, 'menu.management.support', 'f0010030-0001-4000-8000-000000000030', 'lucide:life-buoy', '/support', 0, 0, 1, NULL, '/food-labeling/management/support/index', 0, NULL, UUID()),
+('f0010039-0001-4000-8000-000000000039', 0, NOW(), NULL, NULL, NULL, 29, 1, 'API', 'FoodLabelingApi', 1, 'menu.management.api', 'f0010030-0001-4000-8000-000000000030', 'lucide:code-2', '/api-settings', 0, 0, 1, NULL, '/food-labeling/modules/api/index', 0, NULL, UUID())
+ON DUPLICATE KEY UPDATE
+ `IsDeleted` = VALUES(`IsDeleted`),
+ `LastModificationTime` = NOW(),
+ `OrderNum` = VALUES(`OrderNum`),
+ `State` = VALUES(`State`),
+ `MenuName` = VALUES(`MenuName`),
+ `RouterName` = VALUES(`RouterName`),
+ `MenuType` = VALUES(`MenuType`),
+ `PermissionCode` = VALUES(`PermissionCode`),
+ `ParentId` = VALUES(`ParentId`),
+ `MenuIcon` = VALUES(`MenuIcon`),
+ `Router` = VALUES(`Router`),
+ `IsLink` = VALUES(`IsLink`),
+ `IsCache` = VALUES(`IsCache`),
+ `IsShow` = VALUES(`IsShow`),
+ `Remark` = VALUES(`Remark`),
+ `Component` = VALUES(`Component`),
+ `MenuSource` = VALUES(`MenuSource`),
+ `Query` = VALUES(`Query`),
+ `ConcurrencyStamp` = UUID();
+
+-- 导入后可执行下面语句检查:
+-- SELECT Id, ParentId, MenuName, RouterName, Router, MenuType, MenuSource, OrderNum, IsShow, State
+-- FROM `menu`
+-- WHERE IsDeleted = 0
+-- ORDER BY OrderNum ASC, MenuName ASC;
diff --git a/项目相关文档/批量导入导出接口说明.md b/项目相关文档/批量导入导出接口说明.md
index 453c539..2bc7ec8 100644
--- a/项目相关文档/批量导入导出接口说明.md
+++ b/项目相关文档/批量导入导出接口说明.md
@@ -10,6 +10,7 @@
|------|------|
| [公共约定](#公共约定) | 基址、鉴权、Swagger、通用注意事项 |
| [共享配置](#共享配置) | `appsettings` 中 `FoodLabeling:BatchImport` |
+| [在线批量导入(JSON,推荐)](#在线批量导入json推荐) | Region / Location / Team Member / Product 四模块 JSON 逐行导入 |
| [1 Location Manager(门店)](#1-location-manager门店) | 下载模板 / Excel 导出 / 导入 / 批量编辑 |
| [2 Team Member(成员)](#2-team-member成员) | 下载模板 / **PDF 全量导出** / Excel 导入 / 批量编辑 |
| [3 Products(菜单-产品)](#3-products菜单-产品) | 下载模板 / Excel 全量导出 / Excel 导入 / 批量编辑 |
@@ -27,7 +28,7 @@
- **Swagger 分组**:**「食品标签-美国版接口」**;具体路径以 Swagger 展示为准(下表为常见命名,联调时请以 Swagger 为准)。
- **鉴权**:与其它业务接口相同,请求头携带登录接口返回的 **`data.token` 完整值**(已含 `Bearer ` 前缀),示例:`Authorization: {data.token}`。
- **文件类响应**:`Content-Type` 多为 `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`(`.xlsx`)。
-- **导入类请求**:统一使用 **`multipart/form-data`**,文件字段名以各接口说明为准(Location 为 **`file`**)。
+- **导入类请求**:Excel 导入统一使用 **`multipart/form-data`**,文件字段名以各接口说明为准(Location 为 **`file`**);**推荐**使用下文 [在线批量导入(JSON)](#在线批量导入json推荐) 替代 Excel 上传。
- **批量编辑类请求**:使用 **`application/json`**,一次提交多行(与前端表格「保存全部」对齐)。
- **导出类响应**:Location 与 **Products(菜单-产品)**、**Reports — Print Log** 为 **Excel 全量**;**Team Member**、**Account Management 的 Company / Region**、**Reports — Label Report** 等为 **PDF**(见各小节);数据量极大时请注意服务端内存与响应耗时。
@@ -52,6 +53,81 @@
---
+## 在线批量导入(JSON,推荐)
+
+四个 Account Management / 业务模块均提供 **JSON 在线批量导入** 接口:请求体 `{ "items": [ ...CreateInputVo ] }`,**逐行调用现有 `CreateAsync`**,**部分成功**(单行失败不影响其它行)。`errors` 中 **`index` 从 0 起**(与 `items` 数组下标一致)。单次条数上限为 **`MaxImportRows`**(默认 5000)。
+
+| 模块 | 方法 | HTTP | 路径 | 失败行 key 字段 |
+|------|------|------|------|-----------------|
+| Region(Group) | `BatchImportOnlineAsync` | `POST` | `/api/app/group/batch-import-online` | `groupName` |
+| Location | `BatchImportOnlineAsync` | `POST` | `/api/app/location/batch-import-online` | `locationCode` |
+| Team Member | `BatchImportOnlineAsync` | `POST` | `/api/app/team-member/batch-import-online` | `userName` |
+| Product | `BatchImportOnlineAsync` | `POST` | `/api/app/product/batch-import-online` | `productName` |
+
+**公共返回字段**(各模块 `*BatchImportOnlineResultDto`):`successCount`、`failCount`、`errors[{ index, message, key? }]`(JSON 命名一般为 camelCase)。
+
+**Team Member 特殊规则**:`password` 为空或仅空白时,使用配置 **`TeamMemberImportDefaultPassword`**;若配置亦为空则该行失败。
+
+**与 Excel 导入关系**:Excel 导入接口(`import-*-batch`)**保留但不推荐**,新前端与联调请优先 **`batch-import-online`**;Excel 仍适用于离线模板填写后上传的旧流程。
+
+### Region(Group)在线导入
+
+| 项目 | 说明 |
+|------|------|
+| Body | `GroupBatchImportOnlineInputVo`:`items` 为 `GroupCreateInputVo` 数组(`groupName`、`partnerId`、`state`) |
+| 业务 | 与单条 `POST /api/app/group` 一致;ID 由现有 `CreateAsync` 生成 |
+
+```bash
+curl -X POST "$BASE/api/app/group/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"groupName\":\"NC Region\",\"partnerId\":\"PARTNER_ID\",\"state\":true}]}"
+```
+
+### Location 在线导入
+
+| 项目 | 说明 |
+|------|------|
+| Body | `LocationBatchImportOnlineInputVo`:`items` 为 `LocationCreateInputVo` 数组 |
+| 业务 | 与单条 `POST /api/app/location` 一致 |
+
+```bash
+curl -X POST "$BASE/api/app/location/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"locationCode\":\"LOC001\",\"locationName\":\"UNCC store\",\"partner\":\"MedVantage Cafe Group\",\"groupName\":\"NC Region\",\"state\":true}]}"
+```
+
+### Team Member 在线导入
+
+| 项目 | 说明 |
+|------|------|
+| Body | `TeamMemberBatchImportOnlineInputVo`:`items` 为 `TeamMemberCreateInputVo` 数组 |
+| 密码 | 未填 `password` 时使用 `TeamMemberImportDefaultPassword` |
+
+```bash
+curl -X POST "$BASE/api/app/team-member/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"fullName\":\"John Doe\",\"userName\":\"john@example.com\",\"email\":\"john@example.com\",\"roleId\":\"ROLE_GUID\",\"locationIds\":[\"LOCATION_GUID\"],\"state\":true}]}"
+```
+
+### Product 在线导入
+
+| 项目 | 说明 |
+|------|------|
+| Body | `ProductBatchImportOnlineInputVo`:`items` 为 `ProductCreateInputVo` 数组 |
+| 业务 | 与单条 `POST /api/app/product` 一致 |
+
+```bash
+curl -X POST "$BASE/api/app/product/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"productName\":\"Tuna & Bacon Sub\",\"categoryId\":\"CATEGORY_ID\",\"productCode\":\"40001\",\"state\":true,\"locationIds\":[\"LOCATION_GUID\"]}]}"
+```
+
+---
+
## 1 Location Manager(门店)
**应用服务**:`LocationAppService`(模块 `food-labeling-us`)。
@@ -80,7 +156,7 @@
| 排序 | 与列表一致:有 `Sorting` 则按其排序,否则默认 `CreationTime` 降序 |
| 响应文件名示例 | `locations-export-yyyyMMdd-HHmmss.xlsx` |
-### 1.3 批量导入 Excel
+### 1.3 批量导入 Excel(不推荐,请优先 [batch-import-online](#在线批量导入json推荐))
| 项目 | 说明 |
|------|------|
@@ -182,7 +258,7 @@
**说明**:PDF 中「Assigned Locations」展示该成员**全部**已分配门店(不受列表按门店筛选时「仅显示命中门店」的收缩影响),便于导出后审阅完整权限。
-### 2.3 批量导入 Excel
+### 2.3 批量导入 Excel(不推荐,请优先 [batch-import-online](#在线批量导入json推荐))
| 项目 | 说明 |
|------|------|
@@ -255,7 +331,7 @@
**部署**:须使用 **`dotnet publish` 后的完整输出目录**(含 `ClosedXML.dll`、`DocumentFormat.OpenXml.dll`、`Yi.Abp.Web.deps.json` 等)部署;**不要用 `bin/Debug`(或 `bin/Release`)里挑文件当上线包**。也**禁止**「本地跑起来后只把若干 dll / 自己改过的文件」覆盖到线上(极易漏掉第三方依赖)。上线建议整包替换发布目录,或在服务器上 `git pull` + `dotnet publish`;发布时若输出里缺少 `ClosedXML.dll`,当前 `Yi.Abp.Web` 工程会在 `dotnet publish` 结束时报错提示。
-### 3.3 批量导入 Excel
+### 3.3 批量导入 Excel(不推荐,请优先 [batch-import-online](#在线批量导入json推荐))
| 项目 | 说明 |
|------|------|
@@ -429,6 +505,27 @@ curl -X PUT "$BASE/api/app/product/products-bulk" \
-H "Authorization: TOKEN" \
-H "Content-Type: application/json" \
-d "{\"items\":[{\"id\":\"YOUR_PRODUCT_ID\",\"productName\":\"Tuna & Bacon Sub\",\"categoryId\":\"CATEGORY_ID\",\"productCode\":\"40001\",\"state\":true,\"locationIds\":[\"LOCATION_GUID_1\",\"LOCATION_GUID_2\"]}]}"
+
+# --- 在线批量导入(JSON,推荐)---
+curl -X POST "$BASE/api/app/group/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"groupName\":\"NC Region\",\"partnerId\":\"PARTNER_ID\",\"state\":true}]}"
+
+curl -X POST "$BASE/api/app/location/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"locationCode\":\"LOC001\",\"locationName\":\"UNCC store\",\"state\":true}]}"
+
+curl -X POST "$BASE/api/app/team-member/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"fullName\":\"John\",\"userName\":\"john@example.com\",\"email\":\"john@example.com\",\"roleId\":\"ROLE_GUID\",\"state\":true}]}"
+
+curl -X POST "$BASE/api/app/product/batch-import-online" \
+ -H "Authorization: TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"items\":[{\"productName\":\"Tuna Sub\",\"categoryId\":\"CATEGORY_ID\",\"state\":true}]}"
```
更完整的接口测试流程见仓库内 `.codex/skills/api-interface-testing/SKILL.md`。
--
libgit2 0.21.4