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) + /// + /// 批量导入请求体 + /// 成功数、失败数及失败明细(indexgroupNamemessage + /// 全部或部分行处理完成,见返回体中的计数与 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) + /// + /// 批量导入请求体 + /// 成功数、失败数及失败明细(indexlocationCodemessage + /// 全部或部分行处理完成,见返回体中的计数与 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) + /// + /// 批量导入请求体 + /// 成功数、失败数及失败明细(indexproductNamemessage + /// 全部或部分行处理完成,见返回体中的计数与 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) + /// + /// 批量导入请求体 + /// 成功数、失败数及失败明细(indexuserNamemessage + /// 全部或部分行处理完成,见返回体中的计数与 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