Commit acf259d52f25691ed733e95dc0d8db0fbf0ba5b2
1 parent
899b71eb
2026-07-24
Showing
40 changed files
with
3004 additions
and
84 deletions
泰额版/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 |
| 40 | 40 | /// <inheritdoc /> |
| 41 | 41 | public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync() |
| 42 | 42 | { |
| 43 | - TenantContextGuard.EnsureTenantResolved(CurrentTenant, "获取菜单权限"); | |
| 43 | + // 平台 Token(JWT 无 TenantId)读主库;公司 Token 须已解析租户上下文 | |
| 44 | + if (CurrentTenant.Id.HasValue) | |
| 45 | + { | |
| 46 | + TenantContextGuard.EnsureTenantResolved(CurrentTenant, "获取菜单权限"); | |
| 47 | + } | |
| 48 | + | |
| 44 | 49 | if (!CurrentUser.Id.HasValue) |
| 45 | 50 | { |
| 46 | 51 | throw new UserFriendlyException("用户未登录"); | ... | ... |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs
| 1 | 1 | namespace FoodLabeling.Th.Application.Contracts.Dtos.MultiTenancy; |
| 2 | 2 | |
| 3 | 3 | /// <summary> |
| 4 | -/// 登录页租户下拉项(不含租户 Id 与连接串) | |
| 4 | +/// 登录页租户下拉项(含 Id 供 H5 传 tenantId;不含连接串) | |
| 5 | 5 | /// </summary> |
| 6 | 6 | public class ThTenantSelectDto |
| 7 | 7 | { |
| 8 | + /// <summary>租户 Id(登录 th-web-auth/login 时作为 tenantId)</summary> | |
| 9 | + public Guid Id { get; set; } | |
| 10 | + | |
| 8 | 11 | /// <summary>租户名称</summary> |
| 9 | 12 | public string Name { get; set; } = string.Empty; |
| 10 | 13 | ... | ... |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThWebPlatformLoginHelper.cs
0 → 100644
| 1 | +namespace FoodLabeling.Th.Application.MultiTenancy; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 泰额 H5 同一登录框:Default / 空租户走主库平台登录;具体公司 Guid 走租户业务库。 | |
| 5 | +/// </summary> | |
| 6 | +public static class ThWebPlatformLoginHelper | |
| 7 | +{ | |
| 8 | + /// <summary>主库 Default 租户 Id(业务库可能指向 US,平台登录不得落入该库)</summary> | |
| 9 | + public static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111"); | |
| 10 | + | |
| 11 | + /// <summary>Default 租户显示名</summary> | |
| 12 | + public const string DefaultTenantName = "Default"; | |
| 13 | + | |
| 14 | + /// <summary>平台登录成功时 th-web-auth 返回的 tenantName(无 TenantId Claim)</summary> | |
| 15 | + public const string PlatformTenantDisplayName = "Platform"; | |
| 16 | + | |
| 17 | + /// <summary> | |
| 18 | + /// 是否应优先尝试主库平台登录(与前端选 Default / 空 tenantId 一致) | |
| 19 | + /// </summary> | |
| 20 | + public static bool ShouldTryPlatformLogin(Guid? tenantId, string? tenantName) | |
| 21 | + { | |
| 22 | + if (!tenantId.HasValue || tenantId.Value == Guid.Empty) | |
| 23 | + { | |
| 24 | + return true; | |
| 25 | + } | |
| 26 | + | |
| 27 | + if (tenantId.Value == DefaultTenantId) | |
| 28 | + { | |
| 29 | + return true; | |
| 30 | + } | |
| 31 | + | |
| 32 | + if (!string.IsNullOrWhiteSpace(tenantName) | |
| 33 | + && string.Equals(tenantName.Trim(), DefaultTenantName, StringComparison.OrdinalIgnoreCase)) | |
| 34 | + { | |
| 35 | + return true; | |
| 36 | + } | |
| 37 | + | |
| 38 | + return false; | |
| 39 | + } | |
| 40 | + | |
| 41 | + /// <summary> | |
| 42 | + /// 平台账号须为邮箱(与 account/login 一致) | |
| 43 | + /// </summary> | |
| 44 | + public static bool IsPlausiblePlatformEmail(string userName) | |
| 45 | + { | |
| 46 | + var email = userName.Trim(); | |
| 47 | + return email.Contains('@') | |
| 48 | + && !email.StartsWith('@') | |
| 49 | + && !email.EndsWith('@'); | |
| 50 | + } | |
| 51 | +} | ... | ... |
泰额版/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 |
| 83 | 83 | /// 获取登录页可选租户列表(含默认管理员加密凭据) |
| 84 | 84 | /// </summary> |
| 85 | 85 | /// <remarks> |
| 86 | - /// 供 Web 登录页租户下拉使用;匿名可访问。不返回租户 Id 与数据库连接串。 | |
| 86 | + /// 供 Web 登录页租户下拉使用;匿名可访问。返回租户 Id 与名称(不含数据库连接串)。 | |
| 87 | 87 | /// |
| 88 | 88 | /// 示例响应: |
| 89 | 89 | /// ```json |
| 90 | 90 | /// [ |
| 91 | 91 | /// { |
| 92 | + /// "id": "11111111-1111-1111-1111-111111111111", | |
| 92 | 93 | /// "name": "Default", |
| 93 | 94 | /// "creationTime": "2026-01-01T08:00:00", |
| 94 | 95 | /// "loginAccount": "admin", |
| ... | ... | @@ -99,6 +100,7 @@ public class ThMultiTenancyAppService : ApplicationService, IThMultiTenancyAppSe |
| 99 | 100 | /// ``` |
| 100 | 101 | /// |
| 101 | 102 | /// 字段说明: |
| 103 | + /// - id: 租户 Id,登录 th-web-auth/login 时传 tenantId;选 Default 且用平台邮箱则走主库平台登录 | |
| 102 | 104 | /// - name: 租户名称,登录时可传 tenantName |
| 103 | 105 | /// - creationTime: 租户创建时间 |
| 104 | 106 | /// - loginAccount: 管理员登录账号,优先读凭据表,否则为 admin |
| ... | ... | @@ -126,6 +128,7 @@ public class ThMultiTenancyAppService : ApplicationService, IThMultiTenancyAppSe |
| 126 | 128 | var resolved = ResolveEncryptedCredential(credential); |
| 127 | 129 | return new ThTenantSelectDto |
| 128 | 130 | { |
| 131 | + Id = x.Id, | |
| 129 | 132 | Name = x.Name, |
| 130 | 133 | CreationTime = x.CreationTime, |
| 131 | 134 | LoginAccount = resolved.LoginAccount, | ... | ... |
泰额版/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 |
| 48 | 48 | /// <remarks> |
| 49 | 49 | /// 校验租户后在租户独立库验证账号,签发含 TenantId 与 RBAC 权限的 JWT。 |
| 50 | 50 | /// |
| 51 | - /// 示例请求(tenant-select 不再返回 Id 时使用 tenantName): | |
| 51 | + /// 泰额 H5 与公司 Web 共用此接口。选 **Default** 或 tenantId 为空时,若邮箱账号存在于主库则走**平台登录**(JWT 无 TenantId); | |
| 52 | + /// 选具体公司 tenantId 时在该公司业务库校验。Default 业务库(如 US)仅在主库无该邮箱时作为回落。 | |
| 53 | + /// | |
| 54 | + /// 示例请求(平台,选 Default): | |
| 52 | 55 | /// ```json |
| 53 | 56 | /// { |
| 54 | - /// "tenantName": "Default", | |
| 55 | - /// "userName": "admin", | |
| 56 | - /// "password": "123456", | |
| 57 | - /// "uuid": null, | |
| 58 | - /// "code": null | |
| 57 | + /// "tenantId": "11111111-1111-1111-1111-111111111111", | |
| 58 | + /// "userName": "admin@example.com", | |
| 59 | + /// "password": "123456" | |
| 59 | 60 | /// } |
| 60 | 61 | /// ``` |
| 61 | 62 | /// |
| ... | ... | @@ -82,6 +83,21 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService |
| 82 | 83 | |
| 83 | 84 | ValidationImageCaptcha(input.Uuid, input.Code); |
| 84 | 85 | |
| 86 | + // H5 登录页选 Default / 空 tenantId:优先主库平台账号(admin@example.com),避免误入 Default 业务库(如 US) | |
| 87 | + if (ThWebPlatformLoginHelper.ShouldTryPlatformLogin(input.TenantId, input.TenantName)) | |
| 88 | + { | |
| 89 | + var platformLogin = await TryPlatformLoginAsync(input); | |
| 90 | + if (platformLogin != null) | |
| 91 | + { | |
| 92 | + return platformLogin; | |
| 93 | + } | |
| 94 | + | |
| 95 | + if (!input.TenantId.HasValue || input.TenantId.Value == Guid.Empty) | |
| 96 | + { | |
| 97 | + input.TenantId = ThWebPlatformLoginHelper.DefaultTenantId; | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 85 | 101 | TenantConfiguration tenantConfig; |
| 86 | 102 | try |
| 87 | 103 | { |
| ... | ... | @@ -140,6 +156,43 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService |
| 140 | 156 | } |
| 141 | 157 | } |
| 142 | 158 | |
| 159 | + /// <summary> | |
| 160 | + /// 主库平台登录:CurrentTenant 为空签发无 TenantId 的 JWT,供 Vue3Router/vben5 使用。 | |
| 161 | + /// 主库无对应邮箱用户时返回 null,由调用方回落 Default/公司租户库登录。 | |
| 162 | + /// </summary> | |
| 163 | + private async Task<ThWebLoginOutputDto?> TryPlatformLoginAsync(ThWebLoginInputVo input) | |
| 164 | + { | |
| 165 | + if (!ThWebPlatformLoginHelper.IsPlausiblePlatformEmail(input.UserName)) | |
| 166 | + { | |
| 167 | + return null; | |
| 168 | + } | |
| 169 | + | |
| 170 | + using (CurrentTenant.Change(null)) | |
| 171 | + { | |
| 172 | + var user = await FindActiveUserByEmailAsync(input.UserName.Trim()); | |
| 173 | + if (user == null) | |
| 174 | + { | |
| 175 | + return null; | |
| 176 | + } | |
| 177 | + | |
| 178 | + if (!user.JudgePassword(input.Password)) | |
| 179 | + { | |
| 180 | + throw new UserFriendlyException("登录失败:账号或密码错误"); | |
| 181 | + } | |
| 182 | + | |
| 183 | + var accessToken = await _accountManager.GetTokenByUserIdAsync(user.Id); | |
| 184 | + var refreshToken = _accountManager.CreateRefreshToken(user.Id); | |
| 185 | + | |
| 186 | + return new ThWebLoginOutputDto | |
| 187 | + { | |
| 188 | + Token = accessToken, | |
| 189 | + RefreshToken = refreshToken, | |
| 190 | + TenantId = Guid.Empty, | |
| 191 | + TenantName = ThWebPlatformLoginHelper.PlatformTenantDisplayName | |
| 192 | + }; | |
| 193 | + } | |
| 194 | + } | |
| 195 | + | |
| 143 | 196 | private void ValidationImageCaptcha(string? uuid, string? code) |
| 144 | 197 | { |
| 145 | 198 | if (!_rbacOptions.EnableCaptcha) | ... | ... |
泰额版/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 |
| 514 | 514 | } |
| 515 | 515 | else if ( routerType == "vben5") |
| 516 | 516 | { |
| 517 | - //将后端菜单转换成前端路由,组件级别需要过滤 | |
| 518 | - output = | |
| 519 | - ObjectMapper.Map<List<MenuDto>, List<MenuAggregateRoot>>(menus.Where(x=>x.MenuSource==MenuSourceEnum.Vben5).ToList()).Vben5RouterBuild(); | |
| 517 | + // 泰额 H5 拉 vben5 路由;主库平台菜单多为 MenuSource=Ruoyi,空则回退 Ruoyi 再 Vben5 构建 | |
| 518 | + var vben5Menus = menus.Where(x => x.MenuSource == MenuSourceEnum.Vben5).ToList(); | |
| 519 | + if (vben5Menus.Count == 0) | |
| 520 | + { | |
| 521 | + vben5Menus = menus.Where(x => x.MenuSource == MenuSourceEnum.Ruoyi).ToList(); | |
| 522 | + } | |
| 523 | + | |
| 524 | + output = ObjectMapper | |
| 525 | + .Map<List<MenuDto>, List<MenuAggregateRoot>>(vben5Menus) | |
| 526 | + .Vben5RouterBuild(); | |
| 520 | 527 | } |
| 521 | 528 | |
| 522 | 529 | return output; | ... | ... |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
| ... | ... | @@ -20,7 +20,7 @@ |
| 20 | 20 | }, |
| 21 | 21 | //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) |
| 22 | 22 | "App": { |
| 23 | - "SelfUrl": "http://192.168.31.248:19002", | |
| 23 | + "SelfUrl": "http://192.168.31.88:19002", | |
| 24 | 24 | "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" |
| 25 | 25 | }, |
| 26 | 26 | //配置 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Region(Group)JSON 在线批量导入请求体 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupBatchImportOnlineInputVo | |
| 7 | +{ | |
| 8 | + /// <summary> | |
| 9 | + /// 待导入行,每元素与单条 <c>POST /api/app/group</c> 的 <see cref="GroupCreateInputVo"/> 一致 | |
| 10 | + /// </summary> | |
| 11 | + public List<GroupCreateInputVo> Items { get; set; } = new(); | |
| 12 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupBatchImportOnlineResultDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Group; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// Region(Group)JSON 在线批量导入结果 | |
| 5 | +/// </summary> | |
| 6 | +public class GroupBatchImportOnlineResultDto | |
| 7 | +{ | |
| 8 | + public int SuccessCount { get; set; } | |
| 9 | + | |
| 10 | + public int FailCount { get; set; } | |
| 11 | + | |
| 12 | + public List<GroupBatchImportOnlineErrorDto> Errors { get; set; } = new(); | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// <summary> | |
| 16 | +/// Region(Group)JSON 在线批量导入单条失败信息 | |
| 17 | +/// </summary> | |
| 18 | +public class GroupBatchImportOnlineErrorDto | |
| 19 | +{ | |
| 20 | + /// <summary> | |
| 21 | + /// 在请求 <c>items</c> 数组中的序号(从 0 开始) | |
| 22 | + /// </summary> | |
| 23 | + public int Index { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// Region 名称(<c>groupName</c>),便于定位失败行 | |
| 27 | + /// </summary> | |
| 28 | + public string? GroupName { get; set; } | |
| 29 | + | |
| 30 | + public string Message { get; set; } = string.Empty; | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Location; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 门店 JSON 在线批量导入请求体 | |
| 5 | +/// </summary> | |
| 6 | +public class LocationBatchImportOnlineInputVo | |
| 7 | +{ | |
| 8 | + /// <summary> | |
| 9 | + /// 待导入行,每元素与单条 <c>POST /api/app/location</c> 的 <see cref="LocationCreateInputVo"/> 一致 | |
| 10 | + /// </summary> | |
| 11 | + public List<LocationCreateInputVo> Items { get; set; } = new(); | |
| 12 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationBatchImportOnlineResultDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Location; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 门店 JSON 在线批量导入结果 | |
| 5 | +/// </summary> | |
| 6 | +public class LocationBatchImportOnlineResultDto | |
| 7 | +{ | |
| 8 | + public int SuccessCount { get; set; } | |
| 9 | + | |
| 10 | + public int FailCount { get; set; } | |
| 11 | + | |
| 12 | + public List<LocationBatchImportOnlineErrorDto> Errors { get; set; } = new(); | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// <summary> | |
| 16 | +/// 门店 JSON 在线批量导入单条失败信息 | |
| 17 | +/// </summary> | |
| 18 | +public class LocationBatchImportOnlineErrorDto | |
| 19 | +{ | |
| 20 | + /// <summary> | |
| 21 | + /// 在请求 <c>items</c> 数组中的序号(从 0 开始) | |
| 22 | + /// </summary> | |
| 23 | + public int Index { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 门店编码(<c>locationCode</c>),便于定位失败行 | |
| 27 | + /// </summary> | |
| 28 | + public string? LocationCode { get; set; } | |
| 29 | + | |
| 30 | + public string Message { get; set; } = string.Empty; | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Product; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 产品 JSON 在线批量导入请求体 | |
| 5 | +/// </summary> | |
| 6 | +public class ProductBatchImportOnlineInputVo | |
| 7 | +{ | |
| 8 | + /// <summary> | |
| 9 | + /// 待导入行,每元素与单条 <c>POST /api/app/product</c> 的 <see cref="ProductCreateInputVo"/> 一致 | |
| 10 | + /// </summary> | |
| 11 | + public List<ProductCreateInputVo> Items { get; set; } = new(); | |
| 12 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Product; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 产品 JSON 在线批量导入结果 | |
| 5 | +/// </summary> | |
| 6 | +public class ProductBatchImportOnlineResultDto | |
| 7 | +{ | |
| 8 | + public int SuccessCount { get; set; } | |
| 9 | + | |
| 10 | + public int FailCount { get; set; } | |
| 11 | + | |
| 12 | + public List<ProductBatchImportOnlineErrorDto> Errors { get; set; } = new(); | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// <summary> | |
| 16 | +/// 产品 JSON 在线批量导入单条失败信息 | |
| 17 | +/// </summary> | |
| 18 | +public class ProductBatchImportOnlineErrorDto | |
| 19 | +{ | |
| 20 | + /// <summary> | |
| 21 | + /// 在请求 <c>items</c> 数组中的序号(从 0 开始) | |
| 22 | + /// </summary> | |
| 23 | + public int Index { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 产品名称(<c>productName</c>),便于定位失败行 | |
| 27 | + /// </summary> | |
| 28 | + public string? ProductName { get; set; } | |
| 29 | + | |
| 30 | + public string Message { get; set; } = string.Empty; | |
| 31 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 成员 JSON 在线批量导入请求体 | |
| 5 | +/// </summary> | |
| 6 | +public class TeamMemberBatchImportOnlineInputVo | |
| 7 | +{ | |
| 8 | + /// <summary> | |
| 9 | + /// 待导入行,每元素与单条 <c>POST /api/app/team-member</c> 的 <see cref="TeamMemberCreateInputVo"/> 一致 | |
| 10 | + /// </summary> | |
| 11 | + public List<TeamMemberCreateInputVo> Items { get; set; } = new(); | |
| 12 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 成员 JSON 在线批量导入结果 | |
| 5 | +/// </summary> | |
| 6 | +public class TeamMemberBatchImportOnlineResultDto | |
| 7 | +{ | |
| 8 | + public int SuccessCount { get; set; } | |
| 9 | + | |
| 10 | + public int FailCount { get; set; } | |
| 11 | + | |
| 12 | + public List<TeamMemberBatchImportOnlineErrorDto> Errors { get; set; } = new(); | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// <summary> | |
| 16 | +/// 成员 JSON 在线批量导入单条失败信息 | |
| 17 | +/// </summary> | |
| 18 | +public class TeamMemberBatchImportOnlineErrorDto | |
| 19 | +{ | |
| 20 | + /// <summary> | |
| 21 | + /// 在请求 <c>items</c> 数组中的序号(从 0 开始) | |
| 22 | + /// </summary> | |
| 23 | + public int Index { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 登录账号(<c>userName</c>),便于定位失败行 | |
| 27 | + /// </summary> | |
| 28 | + public string? UserName { get; set; } | |
| 29 | + | |
| 30 | + public string Message { get; set; } = string.Empty; | |
| 31 | +} | ... | ... |
美国版/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 |
| 28 | 28 | public List<string>? PartnerIds { get; set; } |
| 29 | 29 | |
| 30 | 30 | /// <summary> |
| 31 | - /// 适用 Region 多选(<c>fl_group.Id</c>);Company Admin 仅传 Company 时可省略,后端自动绑定该公司下全部 Region 与门店 | |
| 31 | + /// 适用 Region 多选(<c>fl_group.Id</c>);可含 <c>ALL</c> 哨兵(大小写不敏感,与具体 Guid 同传时以 ALL 为准)。 | |
| 32 | + /// Company Admin 仅传 Company 时可省略;<c>locationIds</c> 为空且含 ALL 时展开该公司全部 Region 下门店。 | |
| 32 | 33 | /// </summary> |
| 33 | 34 | public List<string>? RegionIds { get; set; } |
| 34 | 35 | |
| 35 | 36 | /// <summary> |
| 36 | - /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | |
| 37 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同);可含 <c>ALL</c>。 | |
| 37 | 38 | /// </summary> |
| 38 | 39 | public List<string>? GroupIds { get; set; } |
| 39 | 40 | |
| 40 | 41 | /// <summary> |
| 41 | - /// 适用门店多选(<c>location.Id</c>);Company Admin 仅传 Company 时可省略,与 Region 合并后写入 <c>userlocation</c> | |
| 42 | + /// 适用门店多选(<c>location.Id</c>);可含 <c>ALL</c> 哨兵,按 <see cref="PartnerId"/>/<see cref="PartnerIds"/> 展开该公司全部门店落库。 | |
| 43 | + /// Company Admin 仅传 Company 时可省略。 | |
| 42 | 44 | /// </summary> |
| 43 | 45 | public List<string>? LocationIds { get; set; } |
| 44 | 46 | |
| 47 | + /// <summary> | |
| 48 | + /// 适用门店多选别名(与 <see cref="LocationIds"/> 合并解析);可含 <c>ALL</c> 或门店 Guid。 | |
| 49 | + /// </summary> | |
| 50 | + public List<string>? Locations { get; set; } | |
| 51 | + | |
| 45 | 52 | public bool State { get; set; } = true; |
| 46 | 53 | } |
| 47 | 54 | ... | ... |
美国版/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 |
| 19 | 19 | /// <summary>适用 Company Id(多选,由绑定门店反推)</summary> |
| 20 | 20 | public List<string> PartnerIds { get; set; } = new(); |
| 21 | 21 | |
| 22 | - /// <summary>适用 Region Id(多选,<c>fl_group.Id</c>)</summary> | |
| 22 | + /// <summary>适用 Region Id(多选,<c>fl_group.Id</c>;覆盖该公司全部 Region 时回显 <c>["ALL"]</c>)</summary> | |
| 23 | 23 | public List<string> RegionIds { get; set; } = new(); |
| 24 | 24 | |
| 25 | 25 | /// <summary>与 <see cref="RegionIds"/> 相同</summary> |
| 26 | 26 | public List<string> GroupIds { get; set; } = new(); |
| 27 | 27 | |
| 28 | + /// <summary>绑定门店 Id;覆盖该公司全部门店时回显 <c>["ALL"]</c>(库中仍存展开 Guid)</summary> | |
| 28 | 29 | public List<string> LocationIds { get; set; } = new(); |
| 29 | 30 | |
| 30 | 31 | public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new(); | ... | ... |
美国版/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 |
| 28 | 28 | public List<string>? PartnerIds { get; set; } |
| 29 | 29 | |
| 30 | 30 | /// <summary> |
| 31 | - /// 适用 Region 多选(<c>fl_group.Id</c>);Company Admin 仅传 Company 时可省略 | |
| 31 | + /// 适用 Region 多选(<c>fl_group.Id</c>);可含 <c>ALL</c> 哨兵(大小写不敏感,与具体 Guid 同传时以 ALL 为准)。 | |
| 32 | 32 | /// </summary> |
| 33 | 33 | public List<string>? RegionIds { get; set; } |
| 34 | 34 | |
| 35 | 35 | /// <summary> |
| 36 | - /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | |
| 36 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同);可含 <c>ALL</c>。 | |
| 37 | 37 | /// </summary> |
| 38 | 38 | public List<string>? GroupIds { get; set; } |
| 39 | 39 | |
| 40 | 40 | /// <summary> |
| 41 | - /// 适用门店多选(<c>location.Id</c>);Company Admin 仅传 Company 时可省略 | |
| 41 | + /// 适用门店多选(<c>location.Id</c>);可含 <c>ALL</c> 哨兵,按 Company 展开全部门店落库。 | |
| 42 | 42 | /// </summary> |
| 43 | 43 | public List<string>? LocationIds { get; set; } |
| 44 | 44 | |
| 45 | + /// <summary> | |
| 46 | + /// 适用门店多选别名(与 <see cref="LocationIds"/> 合并解析);可含 <c>ALL</c> 或门店 Guid。 | |
| 47 | + /// </summary> | |
| 48 | + public List<string>? Locations { get; set; } | |
| 49 | + | |
| 45 | 50 | public bool State { get; set; } = true; |
| 46 | 51 | } |
| 47 | 52 | ... | ... |
美国版/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 |
| 56 | 56 | /// <param name="input">Keyword、PartnerId、State、Sorting;分页字段忽略</param> |
| 57 | 57 | /// <returns>application/pdf</returns> |
| 58 | 58 | Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input); |
| 59 | + | |
| 60 | + /// <summary> | |
| 61 | + /// JSON 在线批量导入 Region(逐行调用 <see cref="CreateAsync"/>,部分成功) | |
| 62 | + /// </summary> | |
| 63 | + /// <remarks> | |
| 64 | + /// 请求体为 JSON,每行字段与单条新增 <see cref="GroupCreateInputVo"/> 一致。 | |
| 65 | + /// | |
| 66 | + /// 示例请求: | |
| 67 | + /// ```json | |
| 68 | + /// { | |
| 69 | + /// "items": [ | |
| 70 | + /// { | |
| 71 | + /// "groupName": "NC Region", | |
| 72 | + /// "partnerId": "PARTNER_ID", | |
| 73 | + /// "state": true | |
| 74 | + /// } | |
| 75 | + /// ] | |
| 76 | + /// } | |
| 77 | + /// ``` | |
| 78 | + /// | |
| 79 | + /// 参数说明: | |
| 80 | + /// - items: 待导入行数组;<c>index</c> 从 0 起;单次最多 <c>MaxImportRows</c> 条(默认 5000) | |
| 81 | + /// </remarks> | |
| 82 | + /// <param name="input">批量导入请求体</param> | |
| 83 | + /// <returns>成功数、失败数及失败明细(<c>index</c>、<c>groupName</c>、<c>message</c>)</returns> | |
| 84 | + /// <response code="200">全部或部分行处理完成,见返回体中的计数与 errors</response> | |
| 85 | + /// <response code="400">整单校验失败(如 items 为空、超过单次条数上限)</response> | |
| 86 | + /// <response code="500">服务器错误</response> | |
| 87 | + Task<GroupBatchImportOnlineResultDto> BatchImportOnlineAsync(GroupBatchImportOnlineInputVo input); | |
| 59 | 88 | } | ... | ... |
美国版/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 |
| 90 | 90 | /// <response code="400">整单校验失败(如超过单次条数上限、items 为空)</response> |
| 91 | 91 | /// <response code="500">服务器错误</response> |
| 92 | 92 | Task<LocationBulkUpdateResultDto> UpdateLocationsBulkAsync(LocationBulkUpdateInputVo input); |
| 93 | + | |
| 94 | + /// <summary> | |
| 95 | + /// JSON 在线批量导入门店(逐行调用 <see cref="CreateAsync"/>,部分成功) | |
| 96 | + /// </summary> | |
| 97 | + /// <remarks> | |
| 98 | + /// 请求体为 JSON,每行字段与单条新增 <see cref="LocationCreateInputVo"/> 一致。 | |
| 99 | + /// | |
| 100 | + /// 示例请求: | |
| 101 | + /// ```json | |
| 102 | + /// { | |
| 103 | + /// "items": [ | |
| 104 | + /// { | |
| 105 | + /// "locationCode": "LOC001", | |
| 106 | + /// "locationName": "UNCC store", | |
| 107 | + /// "partner": "MedVantage Cafe Group", | |
| 108 | + /// "groupName": "NC Region", | |
| 109 | + /// "state": true | |
| 110 | + /// } | |
| 111 | + /// ] | |
| 112 | + /// } | |
| 113 | + /// ``` | |
| 114 | + /// | |
| 115 | + /// 参数说明: | |
| 116 | + /// - items: 待导入行数组;<c>index</c> 从 0 起;单次最多 <c>MaxImportRows</c> 条(默认 5000) | |
| 117 | + /// </remarks> | |
| 118 | + /// <param name="input">批量导入请求体</param> | |
| 119 | + /// <returns>成功数、失败数及失败明细(<c>index</c>、<c>locationCode</c>、<c>message</c>)</returns> | |
| 120 | + /// <response code="200">全部或部分行处理完成,见返回体中的计数与 errors</response> | |
| 121 | + /// <response code="400">整单校验失败(如 items 为空、超过单次条数上限)</response> | |
| 122 | + /// <response code="500">服务器错误</response> | |
| 123 | + Task<LocationBatchImportOnlineResultDto> BatchImportOnlineAsync(LocationBatchImportOnlineInputVo input); | |
| 93 | 124 | } |
| 94 | 125 | ... | ... |
美国版/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 |
| 74 | 74 | /// 批量编辑产品(JSON 一次提交多行,与单条 <c>PUT</c> 字段一致) |
| 75 | 75 | /// </summary> |
| 76 | 76 | Task<ProductBulkUpdateResultDto> UpdateProductsBulkAsync(ProductBulkUpdateInputVo input); |
| 77 | + | |
| 78 | + /// <summary> | |
| 79 | + /// JSON 在线批量导入产品(逐行调用 <see cref="CreateAsync"/>,部分成功) | |
| 80 | + /// </summary> | |
| 81 | + /// <remarks> | |
| 82 | + /// 请求体为 JSON,每行字段与单条新增 <see cref="ProductCreateInputVo"/> 一致。 | |
| 83 | + /// | |
| 84 | + /// 示例请求: | |
| 85 | + /// ```json | |
| 86 | + /// { | |
| 87 | + /// "items": [ | |
| 88 | + /// { | |
| 89 | + /// "productName": "Tuna & Bacon Sub", | |
| 90 | + /// "categoryId": "CATEGORY_ID", | |
| 91 | + /// "productCode": "40001", | |
| 92 | + /// "state": true, | |
| 93 | + /// "locationIds": ["LOCATION_GUID"] | |
| 94 | + /// } | |
| 95 | + /// ] | |
| 96 | + /// } | |
| 97 | + /// ``` | |
| 98 | + /// | |
| 99 | + /// 参数说明: | |
| 100 | + /// - items: 待导入行数组;<c>index</c> 从 0 起;单次最多 <c>MaxImportRows</c> 条(默认 5000) | |
| 101 | + /// </remarks> | |
| 102 | + /// <param name="input">批量导入请求体</param> | |
| 103 | + /// <returns>成功数、失败数及失败明细(<c>index</c>、<c>productName</c>、<c>message</c>)</returns> | |
| 104 | + /// <response code="200">全部或部分行处理完成,见返回体中的计数与 errors</response> | |
| 105 | + /// <response code="400">整单校验失败(如 items 为空、超过单次条数上限)</response> | |
| 106 | + /// <response code="500">服务器错误</response> | |
| 107 | + Task<ProductBatchImportOnlineResultDto> BatchImportOnlineAsync(ProductBatchImportOnlineInputVo input); | |
| 77 | 108 | } |
| 78 | 109 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
| ... | ... | @@ -14,8 +14,14 @@ public interface ITeamMemberAppService |
| 14 | 14 | |
| 15 | 15 | Task<TeamMemberGetOutputDto> GetAsync(Guid id); |
| 16 | 16 | |
| 17 | + /// <summary> | |
| 18 | + /// 新增成员(POST <c>/api/app/team-member</c>)。<c>locationIds</c> / <c>regionIds</c> / <c>groupIds</c> / <c>locations</c> 可传 <c>ALL</c> 哨兵。 | |
| 19 | + /// </summary> | |
| 17 | 20 | Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input); |
| 18 | 21 | |
| 22 | + /// <summary> | |
| 23 | + /// 更新成员(PUT <c>/api/app/team-member/{id}</c>)。范围传参规则与 <see cref="CreateAsync"/> 相同。 | |
| 24 | + /// </summary> | |
| 19 | 25 | Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input); |
| 20 | 26 | |
| 21 | 27 | Task DeleteAsync(Guid id); |
| ... | ... | @@ -39,4 +45,37 @@ public interface ITeamMemberAppService |
| 39 | 45 | /// 批量编辑成员(JSON 一次提交多行) |
| 40 | 46 | /// </summary> |
| 41 | 47 | Task<TeamMemberBulkUpdateResultDto> UpdateTeamMembersBulkAsync(TeamMemberBulkUpdateInputVo input); |
| 48 | + | |
| 49 | + /// <summary> | |
| 50 | + /// JSON 在线批量导入成员(逐行调用 <see cref="CreateAsync"/>,部分成功) | |
| 51 | + /// </summary> | |
| 52 | + /// <remarks> | |
| 53 | + /// 请求体为 JSON,每行字段与单条新增 <see cref="TeamMemberCreateInputVo"/> 一致; | |
| 54 | + /// <c>password</c> 为空时使用配置 <c>TeamMemberImportDefaultPassword</c>。 | |
| 55 | + /// | |
| 56 | + /// 示例请求: | |
| 57 | + /// ```json | |
| 58 | + /// { | |
| 59 | + /// "items": [ | |
| 60 | + /// { | |
| 61 | + /// "fullName": "John Doe", | |
| 62 | + /// "userName": "john@example.com", | |
| 63 | + /// "email": "john@example.com", | |
| 64 | + /// "roleId": "ROLE_GUID", | |
| 65 | + /// "locationIds": ["LOCATION_GUID"], | |
| 66 | + /// "state": true | |
| 67 | + /// } | |
| 68 | + /// ] | |
| 69 | + /// } | |
| 70 | + /// ``` | |
| 71 | + /// | |
| 72 | + /// 参数说明: | |
| 73 | + /// - items: 待导入行数组;<c>index</c> 从 0 起;单次最多 <c>MaxImportRows</c> 条(默认 5000) | |
| 74 | + /// </remarks> | |
| 75 | + /// <param name="input">批量导入请求体</param> | |
| 76 | + /// <returns>成功数、失败数及失败明细(<c>index</c>、<c>userName</c>、<c>message</c>)</returns> | |
| 77 | + /// <response code="200">全部或部分行处理完成,见返回体中的计数与 errors</response> | |
| 78 | + /// <response code="400">整单校验失败(如 items 为空、超过单次条数上限)</response> | |
| 79 | + /// <response code="500">服务器错误</response> | |
| 80 | + Task<TeamMemberBatchImportOnlineResultDto> BatchImportOnlineAsync(TeamMemberBatchImportOnlineInputVo input); | |
| 42 | 81 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs
| ... | ... | @@ -23,22 +23,36 @@ public static class AllScopeBindingHelper |
| 23 | 23 | return selected.Count == 0; |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | - if (selected.Count != universe.Count) | |
| 26 | + var universeSet = new HashSet<string>( | |
| 27 | + universe.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()), | |
| 28 | + StringComparer.OrdinalIgnoreCase); | |
| 29 | + var selectedSet = new HashSet<string>( | |
| 30 | + selected.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()), | |
| 31 | + StringComparer.OrdinalIgnoreCase); | |
| 32 | + | |
| 33 | + if (selectedSet.Count != universeSet.Count) | |
| 27 | 34 | { |
| 28 | 35 | return false; |
| 29 | 36 | } |
| 30 | 37 | |
| 31 | - var set = new HashSet<string>(universe, StringComparer.Ordinal); | |
| 32 | - return selected.All(id => set.Contains(id)); | |
| 38 | + return selectedSet.SetEquals(universeSet); | |
| 33 | 39 | } |
| 34 | 40 | |
| 35 | - /// <summary>解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。</summary> | |
| 41 | + /// <summary> | |
| 42 | + /// 解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。 | |
| 43 | + /// 入参显式 <see cref="ScopeSpecified"/> 时保留 SPECIFIED 并落库全部 Id,不因「当前上下文全选」折叠为 ALL。 | |
| 44 | + /// </summary> | |
| 36 | 45 | public static string ResolveDimensionType( |
| 37 | 46 | string? declaredType, |
| 38 | 47 | IReadOnlyList<string> ids, |
| 39 | 48 | bool hasArrayInPayload, |
| 40 | 49 | bool isFullSelection) |
| 41 | 50 | { |
| 51 | + if (IsDeclaredSpecified(declaredType)) | |
| 52 | + { | |
| 53 | + return ScopeSpecified; | |
| 54 | + } | |
| 55 | + | |
| 42 | 56 | if (isFullSelection || (ids.Count == 0 && IsDeclaredAll(declaredType))) |
| 43 | 57 | { |
| 44 | 58 | return ScopeAll; |
| ... | ... | @@ -61,6 +75,9 @@ public static class AllScopeBindingHelper |
| 61 | 75 | public static bool IsDeclaredAll(string? type) => |
| 62 | 76 | string.Equals((type ?? ScopeAll).Trim(), ScopeAll, StringComparison.OrdinalIgnoreCase); |
| 63 | 77 | |
| 78 | + public static bool IsDeclaredSpecified(string? type) => | |
| 79 | + string.Equals((type ?? string.Empty).Trim(), ScopeSpecified, StringComparison.OrdinalIgnoreCase); | |
| 80 | + | |
| 64 | 81 | /// <summary>全部 Company(<c>fl_partner.Id</c>)。</summary> |
| 65 | 82 | public static async Task<List<string>> ResolveAllPartnerIdsAsync(ISqlSugarClient db) |
| 66 | 83 | { | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs
| ... | ... | @@ -63,17 +63,16 @@ public static class LabelTemplateScopeHelper |
| 63 | 63 | ISqlSugarClient db, |
| 64 | 64 | LabelTemplateCreateInputVo input) |
| 65 | 65 | { |
| 66 | - var partnerIds = NormalizePartnerIds(input); | |
| 67 | 66 | var regionIds = NormalizeRegionIds(input); |
| 68 | 67 | var locationIds = MergeExplicitLocationIds(input); |
| 69 | 68 | |
| 70 | 69 | var (partnerType, partnerIdsForSave) = await AllScopeBindingHelper.NormalizePartnerScopeAsync( |
| 71 | 70 | db, |
| 72 | 71 | input.AppliedPartnerType, |
| 73 | - partnerIds, | |
| 74 | - null, | |
| 72 | + input.PartnerIds, | |
| 73 | + input.CompanyIds, | |
| 75 | 74 | input.PartnerIds is not null || input.CompanyIds is not null); |
| 76 | - partnerIds = partnerIdsForSave; | |
| 75 | + var partnerIds = partnerIdsForSave; | |
| 77 | 76 | |
| 78 | 77 | var partnerContext = string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) |
| 79 | 78 | ? partnerIds |
| ... | ... | @@ -300,6 +299,8 @@ public static class LabelTemplateScopeHelper |
| 300 | 299 | .Where(x => templateIds.Contains(x.TemplateId)) |
| 301 | 300 | .ToListAsync(); |
| 302 | 301 | |
| 302 | + var storedScopeTypes = await LabelTemplateScopeSchemaHelper.GetAppliedScopeTypesMapAsync(db, templateIds); | |
| 303 | + | |
| 303 | 304 | var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList(); |
| 304 | 305 | var regionIdSet = regionLinks.Select(x => x.GroupId).Distinct(StringComparer.Ordinal).ToList(); |
| 305 | 306 | var locationIdSet = locationLinks.Select(x => x.LocationId).Distinct(StringComparer.Ordinal).ToList(); |
| ... | ... | @@ -360,9 +361,16 @@ public static class LabelTemplateScopeHelper |
| 360 | 361 | var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; |
| 361 | 362 | var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll; |
| 362 | 363 | |
| 364 | + if (storedScopeTypes.TryGetValue(template.Id, out var storedTypes)) | |
| 365 | + { | |
| 366 | + partnerType = NormalizeScopeType(storedTypes.PartnerType, partnerType); | |
| 367 | + regionType = NormalizeScopeType(storedTypes.RegionType, regionType); | |
| 368 | + } | |
| 369 | + | |
| 363 | 370 | if (hasExtendedScope |
| 364 | 371 | && pIds.Count == 0 |
| 365 | - && lIds.Count > 0) | |
| 372 | + && lIds.Count > 0 | |
| 373 | + && !string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | |
| 366 | 374 | { |
| 367 | 375 | pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds); |
| 368 | 376 | if (pIds.Count > 0) |
| ... | ... | @@ -373,7 +381,8 @@ public static class LabelTemplateScopeHelper |
| 373 | 381 | |
| 374 | 382 | if (hasExtendedScope |
| 375 | 383 | && rIds.Count == 0 |
| 376 | - && lIds.Count > 0) | |
| 384 | + && lIds.Count > 0 | |
| 385 | + && !string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | |
| 377 | 386 | { |
| 378 | 387 | rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds); |
| 379 | 388 | if (rIds.Count > 0) | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs
| ... | ... | @@ -315,6 +315,77 @@ public static class LabelTemplateScopeSchemaHelper |
| 315 | 315 | return map.TryGetValue(templateId.Trim(), out var text) ? text : null; |
| 316 | 316 | } |
| 317 | 317 | |
| 318 | + /// <summary>批量读取模板 Company/Region 维度 ALL/SPECIFIED;列不存在时返回空字典。</summary> | |
| 319 | + public static async Task<Dictionary<string, (string PartnerType, string RegionType)>> GetAppliedScopeTypesMapAsync( | |
| 320 | + ISqlSugarClient db, | |
| 321 | + IReadOnlyList<string> templateIds) | |
| 322 | + { | |
| 323 | + var result = new Dictionary<string, (string PartnerType, string RegionType)>(StringComparer.Ordinal); | |
| 324 | + if (templateIds.Count == 0) | |
| 325 | + { | |
| 326 | + return result; | |
| 327 | + } | |
| 328 | + | |
| 329 | + var status = await GetStatusAsync(db); | |
| 330 | + if (!status.HasAppliedPartnerTypeColumn && !status.HasAppliedRegionTypeColumn) | |
| 331 | + { | |
| 332 | + return result; | |
| 333 | + } | |
| 334 | + | |
| 335 | + var ids = templateIds.Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 336 | + .Select(x => x.Trim()) | |
| 337 | + .Distinct(StringComparer.Ordinal) | |
| 338 | + .ToArray(); | |
| 339 | + if (ids.Length == 0) | |
| 340 | + { | |
| 341 | + return result; | |
| 342 | + } | |
| 343 | + | |
| 344 | + var rows = await db.Ado.SqlQueryAsync<AppliedScopeTypesRow>( | |
| 345 | + """ | |
| 346 | + SELECT Id, AppliedPartnerType, AppliedRegionType | |
| 347 | + FROM fl_label_template | |
| 348 | + WHERE Id IN (@ids) | |
| 349 | + """, | |
| 350 | + new { ids }); | |
| 351 | + | |
| 352 | + foreach (var row in rows) | |
| 353 | + { | |
| 354 | + if (string.IsNullOrWhiteSpace(row.Id)) | |
| 355 | + { | |
| 356 | + continue; | |
| 357 | + } | |
| 358 | + | |
| 359 | + var partnerType = status.HasAppliedPartnerTypeColumn | |
| 360 | + ? NormalizeScopeTypeValue(row.AppliedPartnerType) | |
| 361 | + : ScopeAll; | |
| 362 | + var regionType = status.HasAppliedRegionTypeColumn | |
| 363 | + ? NormalizeScopeTypeValue(row.AppliedRegionType) | |
| 364 | + : ScopeAll; | |
| 365 | + result[row.Id.Trim()] = (partnerType, regionType); | |
| 366 | + } | |
| 367 | + | |
| 368 | + return result; | |
| 369 | + } | |
| 370 | + | |
| 371 | + private static string NormalizeScopeTypeValue(string? type) | |
| 372 | + { | |
| 373 | + var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant(); | |
| 374 | + return normalized == ScopeSpecified ? ScopeSpecified : ScopeAll; | |
| 375 | + } | |
| 376 | + | |
| 377 | + private const string ScopeAll = "ALL"; | |
| 378 | + private const string ScopeSpecified = "SPECIFIED"; | |
| 379 | + | |
| 380 | + private sealed class AppliedScopeTypesRow | |
| 381 | + { | |
| 382 | + public string Id { get; init; } = string.Empty; | |
| 383 | + | |
| 384 | + public string? AppliedPartnerType { get; init; } | |
| 385 | + | |
| 386 | + public string? AppliedRegionType { get; init; } | |
| 387 | + } | |
| 388 | + | |
| 318 | 389 | /// <summary>已迁移 <c>Contents</c> 列时写入(未迁移则 no-op)。</summary> |
| 319 | 390 | public static async Task SetContentsAsync(ISqlSugarClient db, string templateId, string? contents) |
| 320 | 391 | { | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
| ... | ... | @@ -321,6 +321,94 @@ public static class LocationScopeBindingHelper |
| 321 | 321 | } |
| 322 | 322 | |
| 323 | 323 | /// <summary> |
| 324 | + /// 多选 Id 是否含 <c>ALL</c> 哨兵(大小写不敏感)。与具体 Guid 同传时以 ALL 为准(全选)。 | |
| 325 | + /// </summary> | |
| 326 | + public static bool IsAllScopeSentinel(string? value) => | |
| 327 | + string.Equals(value?.Trim(), AllScopeBindingHelper.ScopeAll, StringComparison.OrdinalIgnoreCase); | |
| 328 | + | |
| 329 | + /// <summary>Id 列表是否含 <c>ALL</c> 哨兵。</summary> | |
| 330 | + public static bool ContainsAllScopeSentinel(IReadOnlyList<string>? ids) => | |
| 331 | + ids?.Any(IsAllScopeSentinel) == true; | |
| 332 | + | |
| 333 | + /// <summary>去掉 ALL 哨兵,仅保留具体 Id。</summary> | |
| 334 | + public static List<string> FilterConcreteScopeIds(IReadOnlyList<string>? ids) => | |
| 335 | + NormalizeIds(ids).Where(x => !IsAllScopeSentinel(x)).ToList(); | |
| 336 | + | |
| 337 | + /// <summary> | |
| 338 | + /// Team Member 门店范围落库(不做 partner+region+location 并集)。 | |
| 339 | + /// 优先级: | |
| 340 | + /// 1) <c>locationIds</c> 含 ALL → 公司全部门店; | |
| 341 | + /// 2) 有具体 <c>locationIds</c>,且 <c>regionIds</c> 为空或为 ALL → 只绑这些门店(UI 在 Region=ALL 下再收窄门店); | |
| 342 | + /// 3) <c>regionIds</c> 含 ALL → 公司全部 Region 下门店; | |
| 343 | + /// 4) 具体 <c>regionIds</c> → 按 Region 展开(忽略同传 locationIds,保证多区域能落库); | |
| 344 | + /// 5) 仅 <paramref name="partnerIds"/> → 公司全部门店。 | |
| 345 | + /// </summary> | |
| 346 | + public static async Task<List<string>> ResolveTeamMemberLocationIdsForSaveAsync( | |
| 347 | + ISqlSugarClient db, | |
| 348 | + IReadOnlyList<string>? partnerIds, | |
| 349 | + IReadOnlyList<string>? regionIds, | |
| 350 | + IReadOnlyList<string>? locationIds) | |
| 351 | + { | |
| 352 | + var normalizedPartners = NormalizeIds(partnerIds); | |
| 353 | + var locationHasAll = ContainsAllScopeSentinel(locationIds); | |
| 354 | + var concreteLocations = FilterConcreteScopeIds(locationIds); | |
| 355 | + var regionHasAll = ContainsAllScopeSentinel(regionIds); | |
| 356 | + var concreteRegions = FilterConcreteScopeIds(regionIds); | |
| 357 | + | |
| 358 | + // 1. locationIds 含 ALL → 按 partner 展开全部门店 | |
| 359 | + if (locationHasAll) | |
| 360 | + { | |
| 361 | + if (normalizedPartners.Count == 0) | |
| 362 | + { | |
| 363 | + throw new UserFriendlyException("选择全部门店时需指定 Company(partnerId / partnerIds)"); | |
| 364 | + } | |
| 365 | + | |
| 366 | + return await ResolveLocationIdsFromPartnerIdsAsync(db, normalizedPartners); | |
| 367 | + } | |
| 368 | + | |
| 369 | + // 2. 具体门店 +(无区域 / 区域为 ALL)→ 只绑这些门店,避免 regionIds=ALL 盖掉单店 | |
| 370 | + if (concreteLocations.Count > 0 && (regionHasAll || concreteRegions.Count == 0)) | |
| 371 | + { | |
| 372 | + return concreteLocations; | |
| 373 | + } | |
| 374 | + | |
| 375 | + // 3. regionIds 含 ALL(且无具体门店)→ 按 partner 展开全部 Region 下门店 | |
| 376 | + if (regionHasAll) | |
| 377 | + { | |
| 378 | + if (normalizedPartners.Count == 0) | |
| 379 | + { | |
| 380 | + throw new UserFriendlyException("选择全部 Region 时需指定 Company(partnerId / partnerIds)"); | |
| 381 | + } | |
| 382 | + | |
| 383 | + return await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, normalizedPartners, null); | |
| 384 | + } | |
| 385 | + | |
| 386 | + // 4. 具体 regionIds → 按 Region 展开门店(忽略同传 locationIds) | |
| 387 | + if (concreteRegions.Count > 0) | |
| 388 | + { | |
| 389 | + return await MergeToLocationIdsAsync( | |
| 390 | + db, | |
| 391 | + (IReadOnlyList<string>?)null, | |
| 392 | + concreteRegions, | |
| 393 | + null); | |
| 394 | + } | |
| 395 | + | |
| 396 | + // 5. 仅有具体 locationIds → 只绑这些门店 | |
| 397 | + if (concreteLocations.Count > 0) | |
| 398 | + { | |
| 399 | + return concreteLocations; | |
| 400 | + } | |
| 401 | + | |
| 402 | + // 6. 仅有 partner → 绑该公司全部门店 | |
| 403 | + if (normalizedPartners.Count > 0) | |
| 404 | + { | |
| 405 | + return await MergeToLocationIdsAsync(db, normalizedPartners, null, null); | |
| 406 | + } | |
| 407 | + | |
| 408 | + return new List<string>(); | |
| 409 | + } | |
| 410 | + | |
| 411 | + /// <summary> | |
| 324 | 412 | /// 标签类型/分类/多选项门店范围落库:仅按 Region/Location 合并,不因 Company(partner)展开并集。 |
| 325 | 413 | /// </summary> |
| 326 | 414 | public static async Task<List<string>> ResolveEntityLocationIdsForSaveAsync( | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs
| ... | ... | @@ -40,7 +40,7 @@ public static class TeamMemberScopeDisplayHelper |
| 40 | 40 | return assigned.ToList(); |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | - if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0) | |
| 43 | + if (partnerIds.Count > 0) | |
| 44 | 44 | { |
| 45 | 45 | var allPartnerLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync( |
| 46 | 46 | db, partnerIds); |
| ... | ... | @@ -51,14 +51,6 @@ public static class TeamMemberScopeDisplayHelper |
| 51 | 51 | } |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | - var universeLocationIds = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 55 | - db, partnerIds, regionIds, null); | |
| 56 | - if (universeLocationIds.Count > 0 && | |
| 57 | - AllScopeBindingHelper.IsFullIdSelection(assignedIds, universeLocationIds)) | |
| 58 | - { | |
| 59 | - return AllLocationDisplay; | |
| 60 | - } | |
| 61 | - | |
| 62 | 54 | return assigned.ToList(); |
| 63 | 55 | } |
| 64 | 56 | |
| ... | ... | @@ -92,6 +84,43 @@ public static class TeamMemberScopeDisplayHelper |
| 92 | 84 | : id)); |
| 93 | 85 | } |
| 94 | 86 | |
| 87 | + /// <summary> | |
| 88 | + /// 编辑回显:库中存展开后的 Guid;若绑定已覆盖 Company 下全部 Region/门店,则将对应 Id 列表折叠为 <c>["ALL"]</c>,供前端勾选 ALL。 | |
| 89 | + /// </summary> | |
| 90 | + public static async Task<(List<string> RegionIds, List<string> LocationIds, List<TeamMemberAssignedLocationDto> Assigned)> | |
| 91 | + CollapseScopeIdsToAllSentinelForEditAsync( | |
| 92 | + ISqlSugarClient db, | |
| 93 | + IReadOnlyList<string> partnerIds, | |
| 94 | + IReadOnlyList<string> regionIds, | |
| 95 | + IReadOnlyList<string> locationIds, | |
| 96 | + IReadOnlyList<TeamMemberAssignedLocationDto> assigned) | |
| 97 | + { | |
| 98 | + var partners = LocationScopeBindingHelper.NormalizeIds(partnerIds); | |
| 99 | + var regions = LocationScopeBindingHelper.NormalizeIds(regionIds); | |
| 100 | + var locations = LocationScopeBindingHelper.NormalizeIds(locationIds); | |
| 101 | + var assignedList = assigned?.ToList() ?? new List<TeamMemberAssignedLocationDto>(); | |
| 102 | + | |
| 103 | + if (partners.Count == 0) | |
| 104 | + { | |
| 105 | + return (regions, locations, assignedList); | |
| 106 | + } | |
| 107 | + | |
| 108 | + var allRegionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(db, partners); | |
| 109 | + if (allRegionIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(regions, allRegionIds)) | |
| 110 | + { | |
| 111 | + regions = new List<string> { AllScopeBindingHelper.ScopeAll }; | |
| 112 | + } | |
| 113 | + | |
| 114 | + var allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners); | |
| 115 | + if (allLocationIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(locations, allLocationIds)) | |
| 116 | + { | |
| 117 | + locations = new List<string> { AllScopeBindingHelper.ScopeAll }; | |
| 118 | + assignedList = AllLocationDisplay; | |
| 119 | + } | |
| 120 | + | |
| 121 | + return (regions, locations, assignedList); | |
| 122 | + } | |
| 123 | + | |
| 95 | 124 | public static string FormatLocationTextForList(IReadOnlyList<TeamMemberAssignedLocationDto> assigned) |
| 96 | 125 | { |
| 97 | 126 | if (assigned.Count == 0) | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
| ... | ... | @@ -2,8 +2,10 @@ using FoodLabeling.Application.Helpers; |
| 2 | 2 | using FoodLabeling.Application.Contracts.Dtos.Common; |
| 3 | 3 | using FoodLabeling.Application.Contracts.Dtos.Group; |
| 4 | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 5 | +using FoodLabeling.Application.Options; | |
| 5 | 6 | using FoodLabeling.Application.Services.DbModels; |
| 6 | 7 | using Microsoft.AspNetCore.Mvc; |
| 8 | +using Microsoft.Extensions.Options; | |
| 7 | 9 | using QuestPDF.Fluent; |
| 8 | 10 | using QuestPDF.Helpers; |
| 9 | 11 | using QuestPDF.Infrastructure; |
| ... | ... | @@ -25,11 +27,16 @@ public class GroupAppService : ApplicationService, IGroupAppService |
| 25 | 27 | |
| 26 | 28 | private readonly ISqlSugarDbContext _dbContext; |
| 27 | 29 | private readonly IGuidGenerator _guidGenerator; |
| 30 | + private readonly IOptionsSnapshot<FoodLabelingBatchImportOptions> _batchImportOptions; | |
| 28 | 31 | |
| 29 | - public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | |
| 32 | + public GroupAppService( | |
| 33 | + ISqlSugarDbContext dbContext, | |
| 34 | + IGuidGenerator guidGenerator, | |
| 35 | + IOptionsSnapshot<FoodLabelingBatchImportOptions> batchImportOptions) | |
| 30 | 36 | { |
| 31 | 37 | _dbContext = dbContext; |
| 32 | 38 | _guidGenerator = guidGenerator; |
| 39 | + _batchImportOptions = batchImportOptions; | |
| 33 | 40 | } |
| 34 | 41 | |
| 35 | 42 | /// <inheritdoc /> |
| ... | ... | @@ -264,6 +271,47 @@ public class GroupAppService : ApplicationService, IGroupAppService |
| 264 | 271 | return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; |
| 265 | 272 | } |
| 266 | 273 | |
| 274 | + /// <inheritdoc /> | |
| 275 | + [HttpPost("group/batch-import-online")] | |
| 276 | + public async Task<GroupBatchImportOnlineResultDto> BatchImportOnlineAsync( | |
| 277 | + [FromBody] GroupBatchImportOnlineInputVo input) | |
| 278 | + { | |
| 279 | + if (input?.Items is null || input.Items.Count == 0) | |
| 280 | + { | |
| 281 | + throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)"); | |
| 282 | + } | |
| 283 | + | |
| 284 | + var opt = _batchImportOptions.Value; | |
| 285 | + var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows; | |
| 286 | + if (input.Items.Count > maxRows) | |
| 287 | + { | |
| 288 | + throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交"); | |
| 289 | + } | |
| 290 | + | |
| 291 | + var result = new GroupBatchImportOnlineResultDto(); | |
| 292 | + for (var index = 0; index < input.Items.Count; index++) | |
| 293 | + { | |
| 294 | + var vo = input.Items[index]; | |
| 295 | + try | |
| 296 | + { | |
| 297 | + await CreateAsync(vo); | |
| 298 | + result.SuccessCount++; | |
| 299 | + } | |
| 300 | + catch (UserFriendlyException ex) | |
| 301 | + { | |
| 302 | + result.FailCount++; | |
| 303 | + result.Errors.Add(new GroupBatchImportOnlineErrorDto | |
| 304 | + { | |
| 305 | + Index = index, | |
| 306 | + GroupName = vo.GroupName, | |
| 307 | + Message = ex.Message | |
| 308 | + }); | |
| 309 | + } | |
| 310 | + } | |
| 311 | + | |
| 312 | + return result; | |
| 313 | + } | |
| 314 | + | |
| 267 | 315 | private async Task<ISugarQueryable<FlGroupDbEntity, FlPartnerDbEntity>> BuildGroupJoinedQueryAsync( |
| 268 | 316 | GroupGetListInputVo input) |
| 269 | 317 | { | ... | ... |
美国版/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 |
| 303 | 303 | } |
| 304 | 304 | |
| 305 | 305 | /// <inheritdoc /> |
| 306 | + [HttpPost("location/batch-import-online")] | |
| 307 | + public async Task<LocationBatchImportOnlineResultDto> BatchImportOnlineAsync( | |
| 308 | + [FromBody] LocationBatchImportOnlineInputVo input) | |
| 309 | + { | |
| 310 | + if (input?.Items is null || input.Items.Count == 0) | |
| 311 | + { | |
| 312 | + throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)"); | |
| 313 | + } | |
| 314 | + | |
| 315 | + var opt = _batchImportOptions.Value; | |
| 316 | + var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows; | |
| 317 | + if (input.Items.Count > maxRows) | |
| 318 | + { | |
| 319 | + throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交"); | |
| 320 | + } | |
| 321 | + | |
| 322 | + var result = new LocationBatchImportOnlineResultDto(); | |
| 323 | + for (var index = 0; index < input.Items.Count; index++) | |
| 324 | + { | |
| 325 | + var vo = input.Items[index]; | |
| 326 | + try | |
| 327 | + { | |
| 328 | + await CreateAsync(vo); | |
| 329 | + result.SuccessCount++; | |
| 330 | + } | |
| 331 | + catch (UserFriendlyException ex) | |
| 332 | + { | |
| 333 | + result.FailCount++; | |
| 334 | + result.Errors.Add(new LocationBatchImportOnlineErrorDto | |
| 335 | + { | |
| 336 | + Index = index, | |
| 337 | + LocationCode = vo.LocationCode, | |
| 338 | + Message = ex.Message | |
| 339 | + }); | |
| 340 | + } | |
| 341 | + } | |
| 342 | + | |
| 343 | + return result; | |
| 344 | + } | |
| 345 | + | |
| 346 | + /// <inheritdoc /> | |
| 306 | 347 | public async Task<LocationBulkUpdateResultDto> UpdateLocationsBulkAsync([FromBody] LocationBulkUpdateInputVo input) |
| 307 | 348 | { |
| 308 | 349 | if (input?.Items is null || input.Items.Count == 0) | ... | ... |
美国版/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 |
| 477 | 477 | } |
| 478 | 478 | |
| 479 | 479 | /// <inheritdoc /> |
| 480 | + [HttpPost("product/batch-import-online")] | |
| 481 | + public async Task<ProductBatchImportOnlineResultDto> BatchImportOnlineAsync( | |
| 482 | + [FromBody] ProductBatchImportOnlineInputVo input) | |
| 483 | + { | |
| 484 | + if (input?.Items is null || input.Items.Count == 0) | |
| 485 | + { | |
| 486 | + throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)"); | |
| 487 | + } | |
| 488 | + | |
| 489 | + var opt = _batchImportOptions.Value; | |
| 490 | + var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows; | |
| 491 | + if (input.Items.Count > maxRows) | |
| 492 | + { | |
| 493 | + throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交"); | |
| 494 | + } | |
| 495 | + | |
| 496 | + var result = new ProductBatchImportOnlineResultDto(); | |
| 497 | + for (var index = 0; index < input.Items.Count; index++) | |
| 498 | + { | |
| 499 | + var vo = input.Items[index]; | |
| 500 | + try | |
| 501 | + { | |
| 502 | + await CreateAsync(vo); | |
| 503 | + result.SuccessCount++; | |
| 504 | + } | |
| 505 | + catch (UserFriendlyException ex) | |
| 506 | + { | |
| 507 | + result.FailCount++; | |
| 508 | + result.Errors.Add(new ProductBatchImportOnlineErrorDto | |
| 509 | + { | |
| 510 | + Index = index, | |
| 511 | + ProductName = vo.ProductName, | |
| 512 | + Message = ex.Message | |
| 513 | + }); | |
| 514 | + } | |
| 515 | + } | |
| 516 | + | |
| 517 | + return result; | |
| 518 | + } | |
| 519 | + | |
| 520 | + /// <inheritdoc /> | |
| 480 | 521 | public async Task<ProductBulkUpdateResultDto> UpdateProductsBulkAsync( |
| 481 | 522 | [FromBody] ProductBulkUpdateInputVo input) |
| 482 | 523 | { | ... | ... |
美国版/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 |
| 138 | 138 | scopeLocationIds = assigned.Select(x => x.Id).ToList(); |
| 139 | 139 | } |
| 140 | 140 | |
| 141 | + // 库中存展开 Guid;全选时编辑回显折叠为 ["ALL"],与新增传 ALL 对称 | |
| 142 | + (regionIds, scopeLocationIds, assigned) = | |
| 143 | + await TeamMemberScopeDisplayHelper.CollapseScopeIdsToAllSentinelForEditAsync( | |
| 144 | + _dbContext.SqlSugarClient, partnerIds, regionIds, scopeLocationIds, assigned); | |
| 145 | + | |
| 141 | 146 | return new TeamMemberGetOutputDto |
| 142 | 147 | { |
| 143 | 148 | Id = user.Id, |
| ... | ... | @@ -155,7 +160,63 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 155 | 160 | }; |
| 156 | 161 | } |
| 157 | 162 | |
| 158 | - /// <inheritdoc /> | |
| 163 | + /// <summary> | |
| 164 | + /// 新增成员并写入 <c>userlocation</c> 门店绑定。 | |
| 165 | + /// </summary> | |
| 166 | + /// <remarks> | |
| 167 | + /// 范围传参支持 <c>ALL</c> 哨兵(大小写不敏感),与具体 Guid 同传时以 ALL 为准(全选)。 | |
| 168 | + /// | |
| 169 | + /// 示例请求(Locations 全选): | |
| 170 | + /// ```json | |
| 171 | + /// { | |
| 172 | + /// "fullName": "Jane Doe", | |
| 173 | + /// "userName": "jane@example.com", | |
| 174 | + /// "password": "Pass123!", | |
| 175 | + /// "roleId": "ROLE_GUID", | |
| 176 | + /// "partnerId": "PARTNER_GUID", | |
| 177 | + /// "locationIds": ["ALL"], | |
| 178 | + /// "state": true | |
| 179 | + /// } | |
| 180 | + /// ``` | |
| 181 | + /// | |
| 182 | + /// 示例请求(Region 全选): | |
| 183 | + /// ```json | |
| 184 | + /// { | |
| 185 | + /// "fullName": "John Doe", | |
| 186 | + /// "userName": "john@example.com", | |
| 187 | + /// "password": "Pass123!", | |
| 188 | + /// "roleId": "ROLE_GUID", | |
| 189 | + /// "partnerId": "PARTNER_GUID", | |
| 190 | + /// "regionIds": ["ALL"], | |
| 191 | + /// "state": true | |
| 192 | + /// } | |
| 193 | + /// ``` | |
| 194 | + /// | |
| 195 | + /// 参数说明: | |
| 196 | + /// - partnerId / partnerIds: 展开 ALL 时的 Company 上下文(必填) | |
| 197 | + /// - locationIds / locations: 可含 <c>ALL</c>;按该公司全部门店落库;列表 <c>assignedLocations</c> 展示 <c>All Location</c> | |
| 198 | + /// - regionIds / groupIds: 可含 <c>ALL</c>;为 ALL 且同时有具体 locationIds 时以门店为准;含具体 Guid 时按 Region 展开(忽略同传 locationIds) | |
| 199 | + /// - 优先级:location ALL →(具体 location 且 region 空/ALL)→ region ALL → 具体 region → 具体 location → 仅 Company | |
| 200 | + /// | |
| 201 | + /// 示例请求(编辑多 Region + 同传 location,按 Region 展开): | |
| 202 | + /// ```json | |
| 203 | + /// { | |
| 204 | + /// "fullName": "Jane Doe", | |
| 205 | + /// "userName": "jane@example.com", | |
| 206 | + /// "roleId": "ROLE_GUID", | |
| 207 | + /// "partnerId": "PARTNER_GUID", | |
| 208 | + /// "regionIds": ["REGION_GUID_1", "REGION_GUID_2"], | |
| 209 | + /// "locationIds": ["LOCATION_GUID"], | |
| 210 | + /// "state": true | |
| 211 | + /// } | |
| 212 | + /// ``` | |
| 213 | + /// 落库为两 Region 下门店并集;Get 回显 <c>regionIds</c> 含 2 个 Region。 | |
| 214 | + /// </remarks> | |
| 215 | + /// <param name="input">新增成员请求体</param> | |
| 216 | + /// <returns>创建后的成员详情(含 <c>assignedLocations</c> 展示文案)</returns> | |
| 217 | + /// <response code="200">创建成功</response> | |
| 218 | + /// <response code="400">参数不合法或范围无法解析(如 ALL 未传 Company)</response> | |
| 219 | + /// <response code="500">服务器错误</response> | |
| 159 | 220 | public async Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input) |
| 160 | 221 | { |
| 161 | 222 | var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input, input.RoleId); |
| ... | ... | @@ -186,7 +247,45 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 186 | 247 | return await GetAsync(user.Id); |
| 187 | 248 | } |
| 188 | 249 | |
| 189 | - /// <inheritdoc /> | |
| 250 | + /// <summary> | |
| 251 | + /// 更新成员信息与 <c>userlocation</c> 门店绑定。 | |
| 252 | + /// </summary> | |
| 253 | + /// <remarks> | |
| 254 | + /// 范围传参与 <see cref="CreateAsync"/> 一致:<c>locationIds</c> / <c>regionIds</c> / <c>groupIds</c> / <c>locations</c> 可含 <c>ALL</c> 哨兵; | |
| 255 | + /// 与具体 Guid 同传时以 ALL 为准。含具体 <c>regionIds</c> 时按 Region 展开落库并忽略同传 <c>locationIds</c>。 | |
| 256 | + /// 覆盖 Company 全部门店时列表/详情 <c>assignedLocations</c> 展示 <c>All Location</c>。 | |
| 257 | + /// | |
| 258 | + /// 示例请求(Locations ALL): | |
| 259 | + /// ```json | |
| 260 | + /// { | |
| 261 | + /// "fullName": "Jane Doe", | |
| 262 | + /// "userName": "jane@example.com", | |
| 263 | + /// "roleId": "ROLE_GUID", | |
| 264 | + /// "partnerId": "PARTNER_GUID", | |
| 265 | + /// "locationIds": ["ALL"], | |
| 266 | + /// "state": true | |
| 267 | + /// } | |
| 268 | + /// ``` | |
| 269 | + /// | |
| 270 | + /// 示例请求(多 Region + 同传 location,按 Region 展开): | |
| 271 | + /// ```json | |
| 272 | + /// { | |
| 273 | + /// "fullName": "Jane Doe", | |
| 274 | + /// "userName": "jane@example.com", | |
| 275 | + /// "roleId": "ROLE_GUID", | |
| 276 | + /// "partnerId": "PARTNER_GUID", | |
| 277 | + /// "regionIds": ["REGION_GUID_1", "REGION_GUID_2"], | |
| 278 | + /// "locationIds": ["LOCATION_GUID"], | |
| 279 | + /// "state": true | |
| 280 | + /// } | |
| 281 | + /// ``` | |
| 282 | + /// </remarks> | |
| 283 | + /// <param name="id">成员主键</param> | |
| 284 | + /// <param name="input">更新请求体</param> | |
| 285 | + /// <returns>更新后的成员详情</returns> | |
| 286 | + /// <response code="200">更新成功</response> | |
| 287 | + /// <response code="400">参数不合法或成员不存在</response> | |
| 288 | + /// <response code="500">服务器错误</response> | |
| 190 | 289 | public async Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input) |
| 191 | 290 | { |
| 192 | 291 | var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); |
| ... | ... | @@ -449,6 +548,59 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 449 | 548 | } |
| 450 | 549 | |
| 451 | 550 | /// <inheritdoc /> |
| 551 | + [HttpPost("team-member/batch-import-online")] | |
| 552 | + public async Task<TeamMemberBatchImportOnlineResultDto> BatchImportOnlineAsync( | |
| 553 | + [FromBody] TeamMemberBatchImportOnlineInputVo input) | |
| 554 | + { | |
| 555 | + if (input?.Items is null || input.Items.Count == 0) | |
| 556 | + { | |
| 557 | + throw new UserFriendlyException("请至少提交一条导入数据(items 不能为空)"); | |
| 558 | + } | |
| 559 | + | |
| 560 | + var opt = _batchImportOptions.Value; | |
| 561 | + var maxRows = opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows; | |
| 562 | + if (input.Items.Count > maxRows) | |
| 563 | + { | |
| 564 | + throw new UserFriendlyException($"单次批量导入最多允许 {maxRows} 条,请分批提交"); | |
| 565 | + } | |
| 566 | + | |
| 567 | + var defaultPassword = opt.TeamMemberImportDefaultPassword?.Trim() ?? string.Empty; | |
| 568 | + var result = new TeamMemberBatchImportOnlineResultDto(); | |
| 569 | + for (var index = 0; index < input.Items.Count; index++) | |
| 570 | + { | |
| 571 | + var vo = input.Items[index]; | |
| 572 | + try | |
| 573 | + { | |
| 574 | + if (string.IsNullOrWhiteSpace(vo.Password)) | |
| 575 | + { | |
| 576 | + if (string.IsNullOrEmpty(defaultPassword)) | |
| 577 | + { | |
| 578 | + throw new UserFriendlyException( | |
| 579 | + "未配置默认导入密码 FoodLabeling:BatchImport:TeamMemberImportDefaultPassword"); | |
| 580 | + } | |
| 581 | + | |
| 582 | + vo.Password = defaultPassword; | |
| 583 | + } | |
| 584 | + | |
| 585 | + await CreateAsync(vo); | |
| 586 | + result.SuccessCount++; | |
| 587 | + } | |
| 588 | + catch (UserFriendlyException ex) | |
| 589 | + { | |
| 590 | + result.FailCount++; | |
| 591 | + result.Errors.Add(new TeamMemberBatchImportOnlineErrorDto | |
| 592 | + { | |
| 593 | + Index = index, | |
| 594 | + UserName = vo.UserName, | |
| 595 | + Message = ex.Message | |
| 596 | + }); | |
| 597 | + } | |
| 598 | + } | |
| 599 | + | |
| 600 | + return result; | |
| 601 | + } | |
| 602 | + | |
| 603 | + /// <inheritdoc /> | |
| 452 | 604 | public async Task<TeamMemberBulkUpdateResultDto> UpdateTeamMembersBulkAsync( |
| 453 | 605 | [FromBody] TeamMemberBulkUpdateInputVo input) |
| 454 | 606 | { |
| ... | ... | @@ -801,6 +953,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 801 | 953 | .Distinct(StringComparer.OrdinalIgnoreCase) |
| 802 | 954 | .ToList(); |
| 803 | 955 | |
| 956 | + // 与详情一致:全选时 regionIds/locationIds 回显 ["ALL"] | |
| 957 | + var rawLocationIds = (assigned ?? new List<TeamMemberAssignedLocationDto>()) | |
| 958 | + .Select(x => x.Id) | |
| 959 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 960 | + .Select(x => x!.Trim()) | |
| 961 | + .Distinct(StringComparer.OrdinalIgnoreCase) | |
| 962 | + .ToList(); | |
| 963 | + if (rawLocationIds.Count == 0) | |
| 964 | + { | |
| 965 | + rawLocationIds = locationIdList; | |
| 966 | + } | |
| 967 | + | |
| 968 | + (regionIds, locationIdList, assignedLocations) = | |
| 969 | + await TeamMemberScopeDisplayHelper.CollapseScopeIdsToAllSentinelForEditAsync( | |
| 970 | + _dbContext.SqlSugarClient, partnerIds, regionIds, rawLocationIds, assignedLocations); | |
| 971 | + | |
| 804 | 972 | items.Add(new TeamMemberGetListOutputDto |
| 805 | 973 | { |
| 806 | 974 | Id = u.Id, |
| ... | ... | @@ -844,19 +1012,6 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 844 | 1012 | : await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( |
| 845 | 1013 | _dbContext.SqlSugarClient, locationIds); |
| 846 | 1014 | |
| 847 | - if (Guid.TryParse(userId, out var userGuid) && | |
| 848 | - roleIdByUser.TryGetValue(userGuid, out var roleId) && | |
| 849 | - await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) && | |
| 850 | - partnerIds.Count > 0) | |
| 851 | - { | |
| 852 | - var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync( | |
| 853 | - _dbContext.SqlSugarClient, partnerIds); | |
| 854 | - if (allRegions.Count > 0) | |
| 855 | - { | |
| 856 | - regionIds = allRegions; | |
| 857 | - } | |
| 858 | - } | |
| 859 | - | |
| 860 | 1015 | result[userId] = new TeamMemberScopeIds |
| 861 | 1016 | { |
| 862 | 1017 | PartnerIds = partnerIds, |
| ... | ... | @@ -868,7 +1023,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 868 | 1023 | } |
| 869 | 1024 | |
| 870 | 1025 | /// <summary> |
| 871 | - /// Company Admin 列表/详情:展示所选 Company 下全部 Region 与门店。 | |
| 1026 | + /// 绑定覆盖 Company 下全部门店时,列表/详情展开为全部 Region 并折叠 Location 为 All Location 展示。 | |
| 872 | 1027 | /// </summary> |
| 873 | 1028 | private async Task<(List<string> RegionIds, List<TeamMemberAssignedLocationDto> AssignedLocations)> |
| 874 | 1029 | ApplyCompanyAdminDisplayScopeAsync( |
| ... | ... | @@ -877,16 +1032,32 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 877 | 1032 | List<string> regionIds, |
| 878 | 1033 | List<TeamMemberAssignedLocationDto> assigned) |
| 879 | 1034 | { |
| 880 | - if (!await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) || | |
| 881 | - partnerIds.Count == 0) | |
| 1035 | + if (partnerIds.Count == 0) | |
| 1036 | + { | |
| 1037 | + return (regionIds, assigned); | |
| 1038 | + } | |
| 1039 | + | |
| 1040 | + var assignedIds = assigned | |
| 1041 | + .Select(x => x.Id) | |
| 1042 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 1043 | + .Select(x => x.Trim()) | |
| 1044 | + .Distinct(StringComparer.Ordinal) | |
| 1045 | + .ToList(); | |
| 1046 | + if (assignedIds.Count == 0) | |
| 882 | 1047 | { |
| 883 | 1048 | return (regionIds, assigned); |
| 884 | 1049 | } |
| 885 | 1050 | |
| 886 | - var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync( | |
| 887 | - _dbContext.SqlSugarClient, partnerIds); | |
| 888 | 1051 | var allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync( |
| 889 | 1052 | _dbContext.SqlSugarClient, partnerIds); |
| 1053 | + if (allLocationIds.Count == 0 || | |
| 1054 | + !AllScopeBindingHelper.IsFullIdSelection(assignedIds, allLocationIds)) | |
| 1055 | + { | |
| 1056 | + return (regionIds, assigned); | |
| 1057 | + } | |
| 1058 | + | |
| 1059 | + var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync( | |
| 1060 | + _dbContext.SqlSugarClient, partnerIds); | |
| 890 | 1061 | var allAssigned = await BuildAssignedLocationDtosAsync(allLocationIds); |
| 891 | 1062 | |
| 892 | 1063 | return ( |
| ... | ... | @@ -942,7 +1113,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 942 | 1113 | PartnerIds = input.PartnerIds, |
| 943 | 1114 | RegionIds = input.RegionIds, |
| 944 | 1115 | GroupIds = input.GroupIds, |
| 945 | - LocationIds = input.LocationIds | |
| 1116 | + LocationIds = input.LocationIds, | |
| 1117 | + Locations = input.Locations | |
| 946 | 1118 | }, input.RoleId); |
| 947 | 1119 | |
| 948 | 1120 | private async Task<List<string>> ResolveTeamMemberLocationIdsForSaveAsync( |
| ... | ... | @@ -951,12 +1123,16 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 951 | 1123 | { |
| 952 | 1124 | var partnerIds = NormalizePartnerIds(input); |
| 953 | 1125 | var regionIds = NormalizeRegionIds(input); |
| 954 | - var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); | |
| 1126 | + var mergedLocationInputs = MergeLocationScopeInputs(input); | |
| 1127 | + var locationHasAll = LocationScopeBindingHelper.ContainsAllScopeSentinel(mergedLocationInputs); | |
| 1128 | + var explicitLocationIds = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedLocationInputs); | |
| 1129 | + var regionHasAll = LocationScopeBindingHelper.ContainsAllScopeSentinel(regionIds); | |
| 955 | 1130 | var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminRoleAsync( |
| 956 | 1131 | _dbContext.SqlSugarClient, roleId); |
| 957 | 1132 | |
| 958 | 1133 | if (isCompanyAdmin && partnerIds.Count > 0 && |
| 959 | - regionIds.Count == 0 && explicitLocationIds.Count == 0) | |
| 1134 | + !regionHasAll && regionIds.Count == 0 && | |
| 1135 | + !locationHasAll && explicitLocationIds.Count == 0) | |
| 960 | 1136 | { |
| 961 | 1137 | var fromPartner = await LocationScopeBindingHelper.MergeToLocationIdsAsync( |
| 962 | 1138 | _dbContext.SqlSugarClient, partnerIds, null, null); |
| ... | ... | @@ -969,8 +1145,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 969 | 1145 | return fromPartner; |
| 970 | 1146 | } |
| 971 | 1147 | |
| 972 | - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 973 | - _dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds); | |
| 1148 | + var merged = await LocationScopeBindingHelper.ResolveTeamMemberLocationIdsForSaveAsync( | |
| 1149 | + _dbContext.SqlSugarClient, partnerIds, regionIds, mergedLocationInputs); | |
| 974 | 1150 | |
| 975 | 1151 | if (merged.Count == 0) |
| 976 | 1152 | { |
| ... | ... | @@ -984,6 +1160,23 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 984 | 1160 | return merged; |
| 985 | 1161 | } |
| 986 | 1162 | |
| 1163 | + /// <summary>合并 <see cref="TeamMemberCreateInputVo.LocationIds"/> 与 <see cref="TeamMemberCreateInputVo.Locations"/>(均可含 ALL)。</summary> | |
| 1164 | + private static List<string>? MergeLocationScopeInputs(TeamMemberCreateInputVo input) | |
| 1165 | + { | |
| 1166 | + var merged = new List<string>(); | |
| 1167 | + if (input.LocationIds is { Count: > 0 }) | |
| 1168 | + { | |
| 1169 | + merged.AddRange(input.LocationIds); | |
| 1170 | + } | |
| 1171 | + | |
| 1172 | + if (input.Locations is { Count: > 0 }) | |
| 1173 | + { | |
| 1174 | + merged.AddRange(input.Locations); | |
| 1175 | + } | |
| 1176 | + | |
| 1177 | + return merged.Count > 0 ? merged : null; | |
| 1178 | + } | |
| 1179 | + | |
| 987 | 1180 | private static List<string> NormalizePartnerIds(TeamMemberCreateInputVo input) |
| 988 | 1181 | { |
| 989 | 1182 | var merged = new HashSet<string>(StringComparer.Ordinal); |
| ... | ... | @@ -1020,7 +1213,11 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 1020 | 1213 | { |
| 1021 | 1214 | var now = DateTime.Now; |
| 1022 | 1215 | var userIdString = userId.ToString(); |
| 1023 | - var wanted = locationIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList(); | |
| 1216 | + var wanted = locationIds | |
| 1217 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 1218 | + .Select(x => x.Trim()) | |
| 1219 | + .Distinct(StringComparer.OrdinalIgnoreCase) | |
| 1220 | + .ToList(); | |
| 1024 | 1221 | var currentUserId = CurrentUser?.Id?.ToString(); |
| 1025 | 1222 | |
| 1026 | 1223 | var validCount = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() |
| ... | ... | @@ -1032,17 +1229,24 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 1032 | 1229 | throw new UserFriendlyException("存在无效门店,请刷新后重试"); |
| 1033 | 1230 | } |
| 1034 | 1231 | |
| 1232 | + // 含已逻辑删除行:唯一键 UK_userlocation_user_location 覆盖全历史,不能对已删行再 INSERT | |
| 1035 | 1233 | var existing = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() |
| 1036 | 1234 | .Where(x => x.UserId == userIdString) |
| 1037 | 1235 | .ToListAsync(); |
| 1038 | 1236 | |
| 1039 | - var existingActive = existing.Where(x => !x.IsDeleted).ToList(); | |
| 1040 | - var existingActiveSet = existingActive.Select(x => x.LocationId).ToHashSet(); | |
| 1237 | + var byLocation = existing | |
| 1238 | + .GroupBy(x => (x.LocationId ?? string.Empty).Trim(), StringComparer.OrdinalIgnoreCase) | |
| 1239 | + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.IsDeleted).ThenByDescending(x => x.CreationTime).First(), StringComparer.OrdinalIgnoreCase); | |
| 1041 | 1240 | |
| 1042 | - var toDelete = existingActive.Where(x => !wanted.Contains(x.LocationId)).ToList(); | |
| 1043 | - if (toDelete.Count > 0) | |
| 1241 | + var wantedSet = wanted.ToHashSet(StringComparer.OrdinalIgnoreCase); | |
| 1242 | + | |
| 1243 | + var toSoftDelete = existing | |
| 1244 | + .Where(x => !x.IsDeleted && !wantedSet.Contains((x.LocationId ?? string.Empty).Trim())) | |
| 1245 | + .Select(x => x.Id) | |
| 1246 | + .Distinct() | |
| 1247 | + .ToList(); | |
| 1248 | + if (toSoftDelete.Count > 0) | |
| 1044 | 1249 | { |
| 1045 | - var ids = toDelete.Select(x => x.Id).ToList(); | |
| 1046 | 1250 | await _dbContext.SqlSugarClient.Updateable<UserLocationDbEntity>() |
| 1047 | 1251 | .SetColumns(x => new UserLocationDbEntity |
| 1048 | 1252 | { |
| ... | ... | @@ -1050,14 +1254,25 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 1050 | 1254 | LastModificationTime = now, |
| 1051 | 1255 | LastModifierId = currentUserId |
| 1052 | 1256 | }) |
| 1053 | - .Where(x => ids.Contains(x.Id)) | |
| 1257 | + .Where(x => toSoftDelete.Contains(x.Id)) | |
| 1054 | 1258 | .ExecuteCommandAsync(); |
| 1055 | 1259 | } |
| 1056 | 1260 | |
| 1057 | - var toInsert = wanted.Where(x => !existingActiveSet.Contains(x)).ToList(); | |
| 1058 | - if (toInsert.Count > 0) | |
| 1261 | + var toReviveIds = new List<string>(); | |
| 1262 | + var toInsert = new List<UserLocationDbEntity>(); | |
| 1263 | + foreach (var locationId in wanted) | |
| 1059 | 1264 | { |
| 1060 | - var rows = toInsert.Select(locationId => new UserLocationDbEntity | |
| 1265 | + if (byLocation.TryGetValue(locationId, out var row)) | |
| 1266 | + { | |
| 1267 | + if (row.IsDeleted) | |
| 1268 | + { | |
| 1269 | + toReviveIds.Add(row.Id); | |
| 1270 | + } | |
| 1271 | + | |
| 1272 | + continue; | |
| 1273 | + } | |
| 1274 | + | |
| 1275 | + toInsert.Add(new UserLocationDbEntity | |
| 1061 | 1276 | { |
| 1062 | 1277 | Id = _guidGenerator.Create().ToString(), |
| 1063 | 1278 | IsDeleted = false, |
| ... | ... | @@ -1066,9 +1281,25 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 1066 | 1281 | UserId = userIdString, |
| 1067 | 1282 | LocationId = locationId, |
| 1068 | 1283 | ConcurrencyStamp = string.Empty |
| 1069 | - }).ToList(); | |
| 1284 | + }); | |
| 1285 | + } | |
| 1070 | 1286 | |
| 1071 | - await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | |
| 1287 | + if (toReviveIds.Count > 0) | |
| 1288 | + { | |
| 1289 | + await _dbContext.SqlSugarClient.Updateable<UserLocationDbEntity>() | |
| 1290 | + .SetColumns(x => new UserLocationDbEntity | |
| 1291 | + { | |
| 1292 | + IsDeleted = false, | |
| 1293 | + LastModificationTime = now, | |
| 1294 | + LastModifierId = currentUserId | |
| 1295 | + }) | |
| 1296 | + .Where(x => toReviveIds.Contains(x.Id)) | |
| 1297 | + .ExecuteCommandAsync(); | |
| 1298 | + } | |
| 1299 | + | |
| 1300 | + if (toInsert.Count > 0) | |
| 1301 | + { | |
| 1302 | + await _dbContext.SqlSugarClient.Insertable(toInsert).ExecuteCommandAsync(); | |
| 1072 | 1303 | } |
| 1073 | 1304 | } |
| 1074 | 1305 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
| ... | ... | @@ -20,8 +20,8 @@ |
| 20 | 20 | }, |
| 21 | 21 | //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) |
| 22 | 22 | "App": { |
| 23 | - "SelfUrl": "http://192.168.31.88:19001", | |
| 24 | - "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:5174;http://localhost:5173;http://localhost:3000" | |
| 23 | + "SelfUrl": "http://192.168.31.88:19003", | |
| 24 | + "CorsOrigins": "http://localhost:19003;http://localhost:18000;http://localhost:5666;http://localhost:5174;http://localhost:5173;http://localhost:3000" | |
| 25 | 25 | }, |
| 26 | 26 | //配置 |
| 27 | 27 | "Settings": { |
| ... | ... | @@ -32,7 +32,7 @@ |
| 32 | 32 | "DbList": [ "Sqlite", "Mysql", "Sqlserver", "Oracle", "PostgreSQL" ], |
| 33 | 33 | |
| 34 | 34 | "DbConnOptions": { |
| 35 | - "Url": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-us;uid=netteam;pwd=netteam;CharSet=utf8mb4;", | |
| 35 | + "Url": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-test-us;uid=netteam;pwd=netteam;CharSet=utf8mb4;", | |
| 36 | 36 | "DbType": "Mysql", |
| 37 | 37 | "EnabledReadWrite": false,//是否启用读写分离 |
| 38 | 38 | "EnabledCodeFirst": false,//是否通过代码优先创建数据库表结构 | ... | ... |
项目相关文档/2026-07-23泰额版平台与公司登录拉菜单用法.md
0 → 100644
| 1 | +# 2026-07-23 泰额版:平台 / 公司登录与拉菜单用法 | |
| 2 | + | |
| 3 | +本文说明泰额版 **平台管理员** 与 **公司账号** 应如何登录、如何拉菜单,以及完整传参 / 反参示例。 | |
| 4 | + | |
| 5 | +> 相关文档: | |
| 6 | +> - `2026-07-23泰额版当前登录账号菜单接口.md` | |
| 7 | +> - `2026-07-23泰额版平台端操作公司级账号逻辑.md` | |
| 8 | + | |
| 9 | +--- | |
| 10 | + | |
| 11 | +## 一、先分清两套身份 | |
| 12 | + | |
| 13 | +| 角色 | 登录接口 | 用户所在库 | 拉菜单接口 | 菜单数据来源 | | |
| 14 | +|------|----------|------------|------------|--------------| | |
| 15 | +| **平台管理员** | `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) | | |
| 16 | +| **公司账号** | `POST /api/app/th-web-auth/login`(选具体公司 tenantId) | 该公司业务库 `user` | `GET /api/app/auth-session/my-menus` 或 H5 侧 `Vue3Router/vben5` | **当前租户业务库** `menu` | | |
| 17 | + | |
| 18 | +**H5 同一登录框(2026-07-23 后端兼容):** | |
| 19 | + | |
| 20 | +- 前端**始终**打 `POST /api/app/th-web-auth/login`,不会打 `account/login`。 | |
| 21 | +- 下拉选 **Default**(`tenantId=11111111-1111-1111-1111-111111111111`)+ 邮箱 `admin@example.com` → 后端在主库校验,签发**无 TenantId** 的平台 Token,`tenantName` 返回 `Platform`。 | |
| 22 | +- 下拉选 **具体公司**(如「中国麦当劳公司」)→ 仍在该公司业务库校验,JWT 含 `TenantId`。 | |
| 23 | +- Default 租户业务库可能指向 `antis-foodlabeling-us`;平台邮箱账号**不会**误入 US 库。 | |
| 24 | + | |
| 25 | +**禁止混用:** | |
| 26 | + | |
| 27 | +- 公司 Token 不要用平台-only 的管理接口(无租户上下文) | |
| 28 | +- 平台 Token 调 `auth-session/my-menus` 现已可读主库菜单(兼容);H5 仍优先 `Vue3Router/vben5` | |
| 29 | + | |
| 30 | +Base URL 示例:`http://127.0.0.1:19002`(以部署为准)。 | |
| 31 | + | |
| 32 | +```mermaid | |
| 33 | +flowchart TB | |
| 34 | + subgraph platform [平台管理员] | |
| 35 | + P1[POST account/login] | |
| 36 | + P2[GET account/Vue3Router/ruoyi] | |
| 37 | + PHost[(antis-foodlabeling-host)] | |
| 38 | + P1 --> PHost | |
| 39 | + P2 --> PHost | |
| 40 | + end | |
| 41 | + subgraph company [公司账号] | |
| 42 | + C1[POST th-web-auth/login] | |
| 43 | + C2[GET auth-session/my-menus] | |
| 44 | + CBiz[(租户业务库)] | |
| 45 | + C1 --> CBiz | |
| 46 | + C2 --> CBiz | |
| 47 | + end | |
| 48 | +``` | |
| 49 | + | |
| 50 | +--- | |
| 51 | + | |
| 52 | +## 二、平台管理员 | |
| 53 | + | |
| 54 | +### 2.1 登录 | |
| 55 | + | |
| 56 | +| 项 | 值 | | |
| 57 | +|----|------| | |
| 58 | +| 方法 / 路径 | `POST /api/app/account/login` | | |
| 59 | +| Content-Type | `application/json` | | |
| 60 | +| 说明 | 校验主库用户;登录框按 **邮箱** 匹配(`Email` 优先,其次 `UserName`);值须含 `@` | | |
| 61 | + | |
| 62 | +#### 传参(`LoginInputVo`) | |
| 63 | + | |
| 64 | +| 字段 | 类型 | 必填 | 说明 | | |
| 65 | +|------|------|------|------| | |
| 66 | +| `userName` | string | 是 | **填邮箱**,例如种子账号 `admin@example.com`(字段名虽为 userName) | | |
| 67 | +| `password` | string | 是 | 明文密码;种子默认 `123456` | | |
| 68 | +| `uuid` | string | 视环境 | 验证码会话 Id;未开验证码可省略 | | |
| 69 | +| `code` | string | 视环境 | 图形验证码;未开验证码可省略 | | |
| 70 | + | |
| 71 | +```json | |
| 72 | +{ | |
| 73 | + "userName": "admin@example.com", | |
| 74 | + "password": "123456" | |
| 75 | +} | |
| 76 | +``` | |
| 77 | + | |
| 78 | +#### 反参(`LoginOutputDto`) | |
| 79 | + | |
| 80 | +| 字段 | 类型 | 说明 | | |
| 81 | +|------|------|------| | |
| 82 | +| `token` | string | JWT(后续请求 `Authorization: Bearer {token}`) | | |
| 83 | +| `refreshToken` | string | 刷新令牌 | | |
| 84 | + | |
| 85 | +```json | |
| 86 | +{ | |
| 87 | + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | |
| 88 | + "refreshToken": "..." | |
| 89 | +} | |
| 90 | +``` | |
| 91 | + | |
| 92 | +> 若全局包装了统一响应,实际可能是 `{ "data": { "token": "...", "refreshToken": "..." } }`,以 Swagger / 实际返回为准。 | |
| 93 | + | |
| 94 | +```bash | |
| 95 | +curl -X POST "http://127.0.0.1:19002/api/app/account/login" \ | |
| 96 | + -H "Content-Type: application/json" \ | |
| 97 | + -d "{\"userName\":\"admin@example.com\",\"password\":\"123456\"}" | |
| 98 | +``` | |
| 99 | + | |
| 100 | +### 2.2 拉菜单(路由) | |
| 101 | + | |
| 102 | +| 项 | 值 | | |
| 103 | +|----|------| | |
| 104 | +| 方法 / 路径 | `GET /api/app/account/Vue3Router/ruoyi` | | |
| 105 | +| Header | `Authorization: Bearer {token}` | | |
| 106 | +| 说明 | 将当前用户菜单转为 **Ruoyi** 路由树。主库种子菜单 `MenuSource=0`,须用 **`ruoyi`**,不要用 `vben5`(否则按 `MenuSource=2` 过滤会空) | | |
| 107 | + | |
| 108 | +路径参数 `routerType`: | |
| 109 | + | |
| 110 | +| 值 | 对应 MenuSource | 何时用 | | |
| 111 | +|----|-----------------|--------| | |
| 112 | +| `ruoyi` | 0 | **当前平台种子推荐** | | |
| 113 | +| `pure` | 1 | Pure 前端 | | |
| 114 | +| `vben5` | 2 | 仅当菜单已标成 Vben5 时 | | |
| 115 | + | |
| 116 | +#### 传参 | |
| 117 | + | |
| 118 | +无 Query Body;路径上带 `ruoyi` 即可。 | |
| 119 | + | |
| 120 | +#### 反参 | |
| 121 | + | |
| 122 | +返回 **前端路由结构数组/对象**(由 `Vue3RuoYiRouterBuild` 生成),不是 `auth-session/my-menus` 那种 `menus` 树。字段随框架版本可能含:`name`、`path`、`component`、`meta`、`children` 等。以实际 JSON / Swagger 为准。 | |
| 123 | + | |
| 124 | +超级管理员(用户名 `admin`)会拿到对应 `MenuSource` 下全部未删菜单再构建路由。 | |
| 125 | + | |
| 126 | +```bash | |
| 127 | +curl -G "http://127.0.0.1:19002/api/app/account/Vue3Router/ruoyi" \ | |
| 128 | + -H "Authorization: Bearer <platform-token>" | |
| 129 | +``` | |
| 130 | + | |
| 131 | +### 2.3 平台菜单数据位置 | |
| 132 | + | |
| 133 | +| 库表 | 说明 | | |
| 134 | +|------|------| | |
| 135 | +| `antis-foodlabeling-host`.`menu` | 约 26 条:首页概览、平台管理/SAAS 公司、标签管理…、管理/账户管理… | | |
| 136 | +| `user` / `role` / `rolemenu` | 平台 `admin` 已绑定上述菜单 | | |
| 137 | + | |
| 138 | +种子脚本:`th-tenant-menu-seed.sql`(menu)、`th-platform-user-role-seed.sql`(user/role)。菜单 Id 均为合法 Guid(见下文「平台应有菜单清单」)。 | |
| 139 | + | |
| 140 | +--- | |
| 141 | + | |
| 142 | +## 七、平台应有菜单清单 | |
| 143 | + | |
| 144 | +主库 `antis-foodlabeling-host`.`menu` 共 **26** 条未删食品 SaaS 菜单,`MenuSource=0`(Ruoyi)。根菜单 `ParentId='0'`;子菜单 `ParentId` 为父目录 Guid。 | |
| 145 | + | |
| 146 | +| 菜单名称 | Id | router | routerName | component | menuType | parentId | orderNum | menuIcon | | |
| 147 | +|---|---|---|---|---:|---|---:|---:|---| | |
| 148 | +| 首页概览 | `f0010001-0001-4000-8000-000000000001` | `/analytics` | `FoodLabelingDashboard` | `/food-labeling/dashboard/index` | 1 | `0` | -1 | `lucide:layout-dashboard` | | |
| 149 | +| 平台管理 | `f0010002-0001-4000-8000-000000000002` | `/platform` | `FoodLabelingPlatform` | 空 | 0 | `0` | 5 | `lucide:cloud-cog` | | |
| 150 | +| SAAS 公司 | `f0010003-0001-4000-8000-000000000003` | `/platform/tenants` | `FoodLabelingPlatformTenants` | `/food-labeling/platform/tenants/index` | 1 | `f0010002-0001-4000-8000-000000000002` | 6 | `lucide:building` | | |
| 151 | +| 标签管理 | `f0010010-0001-4000-8000-000000000010` | `/labeling` | `FoodLabelingLabeling` | 空 | 0 | `0` | 10 | `lucide:tags` | | |
| 152 | +| 标签 | `f0010011-0001-4000-8000-000000000011` | `/labels` | `FoodLabelingLabels` | `/food-labeling/labeling/labels/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 11 | `lucide:tag` | | |
| 153 | +| 标签分类 | `f0010012-0001-4000-8000-000000000012` | `/label-categories` | `FoodLabelingLabelCategories` | `/food-labeling/labeling/label-categories/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 12 | `lucide:folder-tree` | | |
| 154 | +| 标签类型 | `f0010013-0001-4000-8000-000000000013` | `/label-types` | `FoodLabelingLabelTypes` | `/food-labeling/labeling/label-types/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 13 | `lucide:layers` | | |
| 155 | +| 标签模板 | `f0010014-0001-4000-8000-000000000014` | `/label-templates` | `FoodLabelingLabelTemplates` | `/food-labeling/labeling/label-templates/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 14 | `lucide:layout-template` | | |
| 156 | +| 多选选项集 | `f0010015-0001-4000-8000-000000000015` | `/multiple-options` | `FoodLabelingMultipleOptions` | `/food-labeling/labeling/multiple-options/index` | 1 | `f0010010-0001-4000-8000-000000000010` | 15 | `lucide:list-checks` | | |
| 157 | +| 业务模块 | `f0010020-0001-4000-8000-000000000020` | `/modules` | `FoodLabelingModules` | 空 | 0 | `0` | 15 | `lucide:boxes` | | |
| 158 | +| 培训 | `f0010021-0001-4000-8000-000000000021` | `/training` | `FoodLabelingTraining` | `/food-labeling/modules/training/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 16 | `lucide:graduation-cap` | | |
| 159 | +| 告警 | `f0010022-0001-4000-8000-000000000022` | `/alerts` | `FoodLabelingAlerts` | `/food-labeling/modules/alerts/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 17 | `lucide:bell` | | |
| 160 | +| 任务 | `f0010023-0001-4000-8000-000000000023` | `/tasks` | `FoodLabelingTasks` | `/food-labeling/modules/tasks/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 18 | `lucide:list-todo` | | |
| 161 | +| 传感器 | `f0010024-0001-4000-8000-000000000024` | `/sensors` | `FoodLabelingSensors` | `/food-labeling/modules/sensors/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 19 | `lucide:activity` | | |
| 162 | +| 食物浪费 | `f0010025-0001-4000-8000-000000000025` | `/food-waste` | `FoodLabelingFoodWaste` | `/food-labeling/modules/food-waste/index` | 1 | `f0010020-0001-4000-8000-000000000020` | 20 | `lucide:apple` | | |
| 163 | +| 电子标签 | `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` | | |
| 164 | +| 管理 | `f0010030-0001-4000-8000-000000000030` | `/management` | `FoodLabelingManagement` | 空 | 0 | `0` | 20 | `lucide:building-2` | | |
| 165 | +| 账户管理 | `f0010031-0001-4000-8000-000000000031` | `/account-management` | `FoodLabelingAccountManagement` | `/food-labeling/management/account-management/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 21 | `lucide:users` | | |
| 166 | +| 系统菜单 | `f0010032-0001-4000-8000-000000000032` | `/system-menu` | `FoodLabelingSystemMenu` | `/food-labeling/management/system-menu/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 22 | `lucide:menu-square` | | |
| 167 | +| 菜单管理 | `f0010033-0001-4000-8000-000000000033` | `/menu-management` | `FoodLabelingMenuManagement` | `/food-labeling/management/menu-management/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 23 | `lucide:utensils` | | |
| 168 | +| 设备 | `f0010034-0001-4000-8000-000000000034` | `/devices` | `FoodLabelingDevices` | `/food-labeling/modules/devices/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 24 | `lucide:smartphone` | | |
| 169 | +| 报表 | `f0010035-0001-4000-8000-000000000035` | `/reports` | `FoodLabelingReports` | `/food-labeling/management/reports/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 25 | `lucide:file-bar-chart` | | |
| 170 | +| 发票 | `f0010036-0001-4000-8000-000000000036` | `/invoices` | `FoodLabelingInvoices` | `/food-labeling/modules/invoices/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 26 | `lucide:receipt` | | |
| 171 | +| 二维码 | `f0010037-0001-4000-8000-000000000037` | `/qr-codes` | `FoodLabelingQrCodes` | `/food-labeling/modules/qr-codes/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 27 | `lucide:qr-code` | | |
| 172 | +| 支持 | `f0010038-0001-4000-8000-000000000038` | `/support` | `FoodLabelingSupport` | `/food-labeling/management/support/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 28 | `lucide:life-buoy` | | |
| 173 | +| API | `f0010039-0001-4000-8000-000000000039` | `/api-settings` | `FoodLabelingApi` | `/food-labeling/modules/api/index` | 1 | `f0010030-0001-4000-8000-000000000030` | 29 | `lucide:code-2` | | |
| 174 | + | |
| 175 | +平台 admin 账号:`UserName=admin`,`Email=admin@example.com`,`Id=f0020001-0001-4000-8000-000000000001`;角色 `Id=f0020002-0001-4000-8000-000000000002`;`rolemenu` 绑定上述 26 条菜单。 | |
| 176 | + | |
| 177 | +--- | |
| 178 | + | |
| 179 | +## 三、公司账号 | |
| 180 | + | |
| 181 | +### 3.1 登录 | |
| 182 | + | |
| 183 | +| 项 | 值 | | |
| 184 | +|----|------| | |
| 185 | +| 方法 / 路径 | `POST /api/app/th-web-auth/login` | | |
| 186 | +| Content-Type | `application/json` | | |
| 187 | +| 说明 | 须指定租户;在**该公司业务库**校验账号,JWT 含 `TenantId` | | |
| 188 | + | |
| 189 | +#### 传参(`ThWebLoginInputVo`) | |
| 190 | + | |
| 191 | +| 字段 | 类型 | 必填 | 说明 | | |
| 192 | +|------|------|------|------| | |
| 193 | +| `tenantId` | guid | 与 `tenantName` 二选一 | 租户 Id(推荐) | | |
| 194 | +| `tenantName` | string | 与 `tenantId` 二选一 | 租户名称,如 `中国麦当劳公司` | | |
| 195 | +| `userName` | string | 是 | 登录账号(可为邮箱或用户名) | | |
| 196 | +| `password` | string | 是 | 明文密码 | | |
| 197 | +| `uuid` / `code` | string | 视环境 | 验证码 | | |
| 198 | + | |
| 199 | +```json | |
| 200 | +{ | |
| 201 | + "tenantId": "3a229a02-77eb-1dcc-afb6-09e2a2b6386c", | |
| 202 | + "userName": "admin", | |
| 203 | + "password": "123456" | |
| 204 | +} | |
| 205 | +``` | |
| 206 | + | |
| 207 | +或: | |
| 208 | + | |
| 209 | +```json | |
| 210 | +{ | |
| 211 | + "tenantName": "中国麦当劳公司", | |
| 212 | + "userName": "admin", | |
| 213 | + "password": "123456" | |
| 214 | +} | |
| 215 | +``` | |
| 216 | + | |
| 217 | +#### 反参(`ThWebLoginOutputDto`) | |
| 218 | + | |
| 219 | +| 字段 | 类型 | 说明 | | |
| 220 | +|------|------|------| | |
| 221 | +| `token` | string | JWT(含租户与 RBAC Claims) | | |
| 222 | +| `refreshToken` | string | 刷新令牌 | | |
| 223 | +| `tenantId` | guid | 当前租户 Id | | |
| 224 | +| `tenantName` | string | 当前租户名称 | | |
| 225 | + | |
| 226 | +```json | |
| 227 | +{ | |
| 228 | + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | |
| 229 | + "refreshToken": "...", | |
| 230 | + "tenantId": "3a229a02-77eb-1dcc-afb6-09e2a2b6386c", | |
| 231 | + "tenantName": "中国麦当劳公司" | |
| 232 | +} | |
| 233 | +``` | |
| 234 | + | |
| 235 | +```bash | |
| 236 | +curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \ | |
| 237 | + -H "Content-Type: application/json" \ | |
| 238 | + -d "{\"tenantId\":\"3a229a02-77eb-1dcc-afb6-09e2a2b6386c\",\"userName\":\"admin\",\"password\":\"123456\"}" | |
| 239 | +``` | |
| 240 | + | |
| 241 | +### 3.2 拉菜单 | |
| 242 | + | |
| 243 | +| 项 | 值 | | |
| 244 | +|----|------| | |
| 245 | +| 方法 / 路径 | `GET /api/app/auth-session/my-menus` | | |
| 246 | +| Header | `Authorization: Bearer {公司 token}` | | |
| 247 | +| 建议 Header | `__tenant: {tenantId}`(与 JWT 中 TenantId 一致更稳妥) | | |
| 248 | +| 说明 | 读**当前租户业务库** `menu` + 角色;用户名为 `admin` 时返回该库全部未删菜单 | | |
| 249 | + | |
| 250 | +#### 传参 | |
| 251 | + | |
| 252 | +无 Body;依赖 Token(及可选 `__tenant`)。 | |
| 253 | + | |
| 254 | +#### 反参(`CurrentUserMenuPermissionsOutputDto`,字段一般为 camelCase) | |
| 255 | + | |
| 256 | +| 字段 | 类型 | 说明 | | |
| 257 | +|------|------|------| | |
| 258 | +| `user` | object | 当前用户简要信息 | | |
| 259 | +| `user.id` | guid/string | 用户 Id | | |
| 260 | +| `user.userName` | string | 登录名 | | |
| 261 | +| `user.nick` | string? | 昵称 | | |
| 262 | +| `user.email` | string? | 邮箱 | | |
| 263 | +| `user.icon` | string? | 头像 | | |
| 264 | +| `roleCodes` | string[] | 角色编码 | | |
| 265 | +| `permissionCodes` | string[] | 权限码(超管常见 `*:*:*`) | | |
| 266 | +| `accessPermissionCodes` | string[] | 角色访问权限编码 | | |
| 267 | +| `menus` | array | **菜单树**(根列表,`children` 嵌套) | | |
| 268 | +| `role` | string | 角色展示名 | | |
| 269 | +| `fullName` | string | 全名 | | |
| 270 | +| `lastUpdated` | datetime? | 供前端刷新缓存 | | |
| 271 | + | |
| 272 | +**菜单节点**(`CurrentUserMenuNodeDto`)常见字段: | |
| 273 | + | |
| 274 | +| 字段 | 说明 | | |
| 275 | +|------|------| | |
| 276 | +| `id` / `parentId` | 菜单 Id、父 Id(根多为 `"0"`) | | |
| 277 | +| `menuName` | 名称 | | |
| 278 | +| `router` / `routerName` / `component` | 路由与组件 | | |
| 279 | +| `menuType` / `menuSource` / `orderNum` | 类型、来源、排序 | | |
| 280 | +| `permissionCode` | 权限码 | | |
| 281 | +| `menuIcon` / `isShow` / `state` | 图标、显示、启用 | | |
| 282 | +| `children` | 子节点数组 | | |
| 283 | + | |
| 284 | +```json | |
| 285 | +{ | |
| 286 | + "user": { | |
| 287 | + "id": "...", | |
| 288 | + "userName": "admin", | |
| 289 | + "nick": "超级管理员", | |
| 290 | + "email": "admin@example.com" | |
| 291 | + }, | |
| 292 | + "roleCodes": ["admin"], | |
| 293 | + "permissionCodes": ["*:*:*"], | |
| 294 | + "accessPermissionCodes": [], | |
| 295 | + "menus": [ | |
| 296 | + { | |
| 297 | + "id": "...", | |
| 298 | + "parentId": "0", | |
| 299 | + "menuName": "系统管理", | |
| 300 | + "router": "/system", | |
| 301 | + "orderNum": 100, | |
| 302 | + "children": [] | |
| 303 | + } | |
| 304 | + ], | |
| 305 | + "role": "管理员", | |
| 306 | + "fullName": "超级管理员", | |
| 307 | + "lastUpdated": "2026-07-23T10:00:00" | |
| 308 | +} | |
| 309 | +``` | |
| 310 | + | |
| 311 | +```bash | |
| 312 | +curl -G "http://127.0.0.1:19002/api/app/auth-session/my-menus" \ | |
| 313 | + -H "Authorization: Bearer <company-token>" \ | |
| 314 | + -H "__tenant: 3a229a02-77eb-1dcc-afb6-09e2a2b6386c" | |
| 315 | +``` | |
| 316 | + | |
| 317 | +未识别租户时典型错误: | |
| 318 | + | |
| 319 | +> 未识别租户上下文。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 `__tenant` 携带租户 Id。 | |
| 320 | + | |
| 321 | +### 3.3 公司库菜单现状说明 | |
| 322 | + | |
| 323 | +| 库 | 当前菜单大致情况 | | |
| 324 | +|----|------------------| | |
| 325 | +| 主库 host | 已种食品 SaaS 中文菜单(平台用) | | |
| 326 | +| Default → `antis-foodlabeling-us` | 美国版英文业务菜单 | | |
| 327 | +| 如 `antis-foodlabeling-t46d56528`(中国麦当劳) | 多为 **Yi 框架**种子(系统管理/用户管理…),**尚未**种 `th-tenant-menu-seed.sql` 那套食品 SaaS 菜单 | | |
| 328 | + | |
| 329 | +若公司端侧边栏也要与平台同一套「首页概览 / 标签管理 / 业务模块…」: | |
| 330 | + | |
| 331 | +1. 对该公司业务库执行 `项目相关文档/th-tenant-menu-seed.sql`(**禁止**对 `antis-foodlabeling-us` 执行,以免污染美国版) | |
| 332 | +2. 为公司管理员角色绑定对应 `rolemenu`(或公司 `admin` 用户名可直接看全库未删菜单) | |
| 333 | +3. 再用 `th-web-auth/login` + `auth-session/my-menus` 验证 | |
| 334 | + | |
| 335 | +--- | |
| 336 | + | |
| 337 | +## 四、对照速查 | |
| 338 | + | |
| 339 | +| 步骤 | 平台管理员(H5 选 Default) | 公司账号 | | |
| 340 | +|------|------------|----------| | |
| 341 | +| 1. 登录 | `POST /api/app/th-web-auth/login`(Default tenantId + 邮箱) | `POST /api/app/th-web-auth/login` | | |
| 342 | +| 2. 关键 | Default `tenantId` + 邮箱 `userName` + `password` | 公司 `tenantId` + `userName` + `password` | | |
| 343 | +| 3. 登录反参 | `token` / `refreshToken` / `tenantName=Platform` | `token` / `refreshToken` / `tenantId` / `tenantName` | | |
| 344 | +| 4. 拉菜单(H5) | `GET /api/app/account/Vue3Router/vben5` | `GET /api/app/auth-session/my-menus` 或 H5 同 vben5 | | |
| 345 | +| 5. 菜单 Header | `Authorization: Bearer …` | `Authorization` + 建议 `__tenant` | | |
| 346 | +| 6. 菜单反参形态 | Vben5 路由树(Ruoyi 回退) | `user` + `menus` 树 + 权限码 | | |
| 347 | + | |
| 348 | +--- | |
| 349 | + | |
| 350 | +## 五、前端对接注意(H5 / Vben5,只读不改源码) | |
| 351 | + | |
| 352 | +1. **登录**:`POST /api/app/th-web-auth/login`(见 `th-web-auth.ts`),Header 带 `__tenant: tenantId`。 | |
| 353 | +2. **登录 Body**:`tenantId`、`userName`(Email 字段)、`password`;可选 `uuid`/`code`(验证码)。 | |
| 354 | +3. **tenant-select**:`GET /api/app/th-multi-tenancy/tenant-select` 现返回 `id` + `name`;前端用 `id` 作为下拉 value。 | |
| 355 | +4. **平台管理员操作**:选 **Default**,邮箱填 `admin@example.com`,密码 `123456`。 | |
| 356 | +5. **公司操作**:选 **中国麦当劳公司** 等公司项,填该公司业务库账号(如 `admin`)。 | |
| 357 | +6. **拉菜单**:H5 dist 实际调 `GET /api/app/account/Vue3Router/vben5`(非 `my-menus`);平台 Token 无租户即可;主库 `MenuSource=0` 时后端自动回退 Ruoyi 再 Vben5 构建。 | |
| 358 | +7. **公司端**仍可用 `auth-session/my-menus`(须 Token + `__tenant`)。 | |
| 359 | +8. 统一响应包装时从 `data` 取 `token` / `menus`。 | |
| 360 | + | |
| 361 | +### 5.1 H5 登录请求示例(平台) | |
| 362 | + | |
| 363 | +```json | |
| 364 | +POST /api/app/th-web-auth/login | |
| 365 | +Header: __tenant: 11111111-1111-1111-1111-111111111111 | |
| 366 | +Body: | |
| 367 | +{ | |
| 368 | + "tenantId": "11111111-1111-1111-1111-111111111111", | |
| 369 | + "userName": "admin@example.com", | |
| 370 | + "password": "123456" | |
| 371 | +} | |
| 372 | +``` | |
| 373 | + | |
| 374 | +成功反参(平台): | |
| 375 | + | |
| 376 | +```json | |
| 377 | +{ | |
| 378 | + "token": "eyJ...", | |
| 379 | + "refreshToken": "...", | |
| 380 | + "tenantId": "00000000-0000-0000-0000-000000000000", | |
| 381 | + "tenantName": "Platform" | |
| 382 | +} | |
| 383 | +``` | |
| 384 | + | |
| 385 | +### 5.2 H5 登录请求示例(公司) | |
| 386 | + | |
| 387 | +```json | |
| 388 | +{ | |
| 389 | + "tenantId": "3a229a02-77eb-1dcc-afb6-09e2a2b6386c", | |
| 390 | + "userName": "admin", | |
| 391 | + "password": "123456" | |
| 392 | +} | |
| 393 | +``` | |
| 394 | + | |
| 395 | +--- | |
| 396 | + | |
| 397 | +## 六、自检清单 | |
| 398 | + | |
| 399 | +- [ ] H5 选 Default + `admin@example.com`:`th-web-auth/login` 成功,`tenantName=Platform`,Token 无 TenantId Claim | |
| 400 | +- [ ] 平台:`Vue3Router/vben5` 返回非空路由(Ruoyi 菜单回退) | |
| 401 | +- [ ] 公司:选公司 tenantId + 公司账号登录未破坏;`my-menus` 带 `__tenant` 成功 | |
| 402 | +- [ ] 公司业务库菜单内容符合产品预期(必要时单独执行租户库 menu 种子,避开 US 库) | ... | ... |
项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md
0 → 100644
| 1 | +# 2026-07-23 泰额版:平台端操作公司级账号逻辑说明 | |
| 2 | + | |
| 3 | +本文档说明 **泰额版** 平台管理端如何操作「公司级」账号与权限:架构、接口、写库路径、与公司端登录的关系,以及**不得影响美国版**的隔离约束。 | |
| 4 | + | |
| 5 | +相关文档: | |
| 6 | + | |
| 7 | +- `2026-07-21代码优化.md`:公司列表 / 改密 / SaaS 菜单与角色菜单接口细节 | |
| 8 | +- `2026-07-23泰额版当前登录账号菜单接口.md`:平台 vs 公司「当前登录人菜单」 | |
| 9 | +- `th-tenant-menu-seed.sql` / `th-platform-user-role-seed.sql`:平台主库菜单与 User/Role 种子 | |
| 10 | + | |
| 11 | +--- | |
| 12 | + | |
| 13 | +## 一、角色与库分层 | |
| 14 | + | |
| 15 | +| 端 | 登录 | 用户库 | 典型能力 | | |
| 16 | +|----|------|--------|----------| | |
| 17 | +| **平台端** | `POST /api/app/account/login` | 主库 `antis-foodlabeling-host`.`user` | 公司列表、改公司管理员、分配 SaaS 菜单、改公司角色菜单、开通/删除公司 | | |
| 18 | +| **公司端** | `POST /api/app/th-web-auth/login`(须 `tenantId`) | 该公司业务库 `user` | 业务功能;`GET /api/app/auth-session/my-menus` | | |
| 19 | + | |
| 20 | +**公司 = 租户**:主库表 `YiTenant`(前端常把 `name` 当 `companyName`)。 | |
| 21 | + | |
| 22 | +```mermaid | |
| 23 | +flowchart LR | |
| 24 | + subgraph platform [平台端] | |
| 25 | + PLogin[account/login] | |
| 26 | + PHost[(antis-foodlabeling-host)] | |
| 27 | + PLogin --> PHost | |
| 28 | + end | |
| 29 | + subgraph company [公司端] | |
| 30 | + CLogin[th-web-auth/login] | |
| 31 | + CBiz[(租户业务库)] | |
| 32 | + CLogin --> CBiz | |
| 33 | + end | |
| 34 | + PHost -->|YiTenant.TenantConnectionString| CBiz | |
| 35 | + PHost -->|改 admin / 角色菜单| CBiz | |
| 36 | +``` | |
| 37 | + | |
| 38 | +### 1.1 库职责 | |
| 39 | + | |
| 40 | +| 库 | 配置来源 | 平台操作相关表 | | |
| 41 | +|----|----------|----------------| | |
| 42 | +| **主库** `antis-foodlabeling-host` | `DbConnOptions.Url` | `YiTenant`、`fl_th_tenant_admin_credential`、`fl_th_tenant_menu_permission`、平台 `user`/`role`/`menu`/`userrole`/`rolemenu` | | |
| 43 | +| **租户业务库** `antis-foodlabeling-{key}` | `YiTenant.TenantConnectionString` | 公司 `User`(登录哈希)、`Role`/`RoleMenu`/`Menu`、业务表 | | |
| 44 | +| **美国版库** `antis-foodlabeling-us` | 美版 `appsettings`;泰额 **Default** 租户连接串也指向此库 | **禁止**当作泰额新公司业务库去写平台种子;对 Default 改 admin/角色会动到美版数据 | | |
| 45 | + | |
| 46 | +### 1.2 两层「菜单」勿混淆 | |
| 47 | + | |
| 48 | +| 层 | 存储 | 接口 | 语义 | | |
| 49 | +|----|------|------|------| | |
| 50 | +| **SaaS 公司开通模块** | 主库 `fl_th_tenant_menu_permission`(`menuPermissionKeys`) | `menu-permission-tree` / `company-menus` | 该公司「能开哪些产品模块」 | | |
| 51 | +| **公司 RBAC 菜单** | 租户库 `Menu` + `RoleMenu` | `company-roles` / `company-role-menus`;公司端 `auth-session/my-menus` | 该公司内角色能看哪些业务菜单 | | |
| 52 | + | |
| 53 | +平台主库种子菜单(`th-menu-*`)服务于**平台端自身路由**,不是公司业务库菜单。 | |
| 54 | + | |
| 55 | +--- | |
| 56 | + | |
| 57 | +## 二、平台端操作公司账号:能力总览 | |
| 58 | + | |
| 59 | +实现类:`ThMultiTenancyAppService`(路由前缀 `/api/app/th-multi-tenancy/`)。 | |
| 60 | +开通公司:`ThTenantProvisioningAppService`(`/api/app/th-tenant-provisioning/`)。 | |
| 61 | + | |
| 62 | +| 能力 | 方法 / 路径 | 主要读写 | | |
| 63 | +|------|-------------|----------| | |
| 64 | +| 登录页租户下拉(匿名) | `GET .../tenant-select` | 读主库租户 + 凭据(无 Id) | | |
| 65 | +| 公司分页列表 | `GET .../company-list` | 读主库租户 + 凭据 + SaaS keys | | |
| 66 | +| 改公司管理员账号/密码 | `PUT .../company-admin` | 写**租户库 User** + 写主库凭据表 | | |
| 67 | +| SaaS 权限树 | `GET .../menu-permission-tree` | 代码目录,无库 | | |
| 68 | +| 查/设公司 SaaS 菜单 | `GET/PUT .../company-menus` | 读写主库 `fl_th_tenant_menu_permission` | | |
| 69 | +| 查公司角色 | `GET .../company-roles` | 读租户库 `Role`/`RoleMenu` | | |
| 70 | +| 设公司角色菜单 | `PUT .../company-role-menus` | 写租户库 `RoleMenu` | | |
| 71 | +| 开通公司 | `POST /th-tenant-provisioning/provision` | 写主库 `YiTenant` + 建业务库/Init | | |
| 72 | +| 删除公司 | `DELETE .../company/{tenantId}` | 软删主库租户;可选 DROP 业务库 | | |
| 73 | + | |
| 74 | +鉴权:除 `tenant-select` 外,平台管理接口需 **平台 Token**(`account/login`)。 | |
| 75 | + | |
| 76 | +--- | |
| 77 | + | |
| 78 | +## 三、核心数据流 | |
| 79 | + | |
| 80 | +### 3.1 开通公司(产生「公司级账号」) | |
| 81 | + | |
| 82 | +```text | |
| 83 | +平台管理员 | |
| 84 | + → POST /api/app/th-tenant-provisioning/provision | |
| 85 | + → 主库写入 YiTenant(Name + TenantConnectionString) | |
| 86 | + → EnsureDatabaseCreated(物理建库) | |
| 87 | + → 后台 InitAsync:业务库 CodeFirst + Seed | |
| 88 | + └─ 默认 User.UserName = admin,密码 = RbacOptions.AdminPassword(哈希) | |
| 89 | +``` | |
| 90 | + | |
| 91 | +要点: | |
| 92 | + | |
| 93 | +- 开通**不会**自动写 `fl_th_tenant_admin_credential`。 | |
| 94 | +- 列表回显密码:无凭据行时用配置默认密码现场 AES 加密;平台首次 `PUT company-admin` 后才持久化可回显密文。 | |
| 95 | +- Init 异步未完成时,立刻改 admin / 公司登录可能失败。 | |
| 96 | + | |
| 97 | +### 3.2 平台改公司管理员(重点) | |
| 98 | + | |
| 99 | +接口:`PUT /api/app/th-multi-tenancy/company-admin` | |
| 100 | + | |
| 101 | +```text | |
| 102 | +1. CurrentTenant=null(主库 scope) | |
| 103 | + 读 YiTenant、fl_th_tenant_admin_credential(当前 LoginAccount,默认 admin) | |
| 104 | +2. 若有 password + passwordSalt:AES 解密得明文 | |
| 105 | +3. 用 YiTenant.TenantConnectionString 建独立 SqlSugarClient(直连业务库,不靠 Change(tenantId) 切仓储) | |
| 106 | +4. 在业务库 User 上: | |
| 107 | + - 可选改 UserName(查重) | |
| 108 | + - 可选 ApplyPlainPassword → 写 Password/Salt 哈希 | |
| 109 | +5. Upsert 主库 fl_th_tenant_admin_credential(LoginAccount + AES 密文 + IV) | |
| 110 | +``` | |
| 111 | + | |
| 112 | +| 写入位置 | 字段语义 | | |
| 113 | +|----------|----------| | |
| 114 | +| 租户库 `User.Password/Salt` | **登录校验用哈希**(不可逆) | | |
| 115 | +| 主库 `PasswordCipher/PasswordIv` | **列表回显用 AES 密文**(可逆,密钥 `FoodLabeling:TenantSelectCrypto`) | | |
| 116 | + | |
| 117 | +前端改密必须先按约定 AES 加密再提交,**禁止**明文密码直传。 | |
| 118 | + | |
| 119 | +### 3.3 平台分配公司 SaaS 菜单 | |
| 120 | + | |
| 121 | +```text | |
| 122 | +GET menu-permission-tree → 静态 Key 树(ThSaasMenuPermissionCatalog) | |
| 123 | +GET company-menus?tenantId= → 读主库已分配 keys | |
| 124 | +PUT company-menus → 主库先删后插 fl_th_tenant_menu_permission | |
| 125 | +``` | |
| 126 | + | |
| 127 | +不写租户库 `Menu`/`RoleMenu`。非法 key 会校验失败。 | |
| 128 | + | |
| 129 | +### 3.4 平台配置公司内角色菜单 | |
| 130 | + | |
| 131 | +```text | |
| 132 | +GET company-roles?tenantId= → 直连租户库读 Role + RoleMenu.menuIds | |
| 133 | +PUT company-role-menus → 直连租户库覆盖 RoleMenu(校验 Menu 存在) | |
| 134 | +``` | |
| 135 | + | |
| 136 | +这与公司员工登录后看到的 `auth-session/my-menus` 同源(租户库 RBAC)。 | |
| 137 | + | |
| 138 | +### 3.5 删除公司 | |
| 139 | + | |
| 140 | +```text | |
| 141 | +DELETE /api/app/th-multi-tenancy/company/{tenantId}?dropDatabase=true | |
| 142 | + → 禁止删除 Default 租户 | |
| 143 | + → DROP 业务库时禁止库名 host / us(及受保护库) | |
| 144 | + → 软删 YiTenant | |
| 145 | + → 删凭据表、SaaS 菜单权限、租户缓存 | |
| 146 | +``` | |
| 147 | + | |
| 148 | +--- | |
| 149 | + | |
| 150 | +## 四、跨库访问约定(实现要点) | |
| 151 | + | |
| 152 | +| 约定 | 说明 | | |
| 153 | +|------|------| | |
| 154 | +| 主库表 | 实体带 `[DefaultTenantTable]`;读写前 `CurrentTenant.Change(null)`,连接走 `DbConnOptions.Url`(host) | | |
| 155 | +| 租户业务库 | `TenantBusinessDatabaseAccessor`:按 `YiTenant` 连接串 **独立 SqlSugarClient**,避免空连接串误连主库 | | |
| 156 | +| 不信任 JWT 租户 | 平台改公司数据时以入参 `tenantId` + 主库登记的连接串为准,不以「当前登录租户」替代目标公司 | | |
| 157 | + | |
| 158 | +--- | |
| 159 | + | |
| 160 | +## 五、与美国版隔离(强制) | |
| 161 | + | |
| 162 | +| 规则 | 说明 | | |
| 163 | +|------|------| | |
| 164 | +| 平台种子只写 **host** | `menu` / `user` / `role` 等平台数据禁止写入 `antis-foodlabeling-us` | | |
| 165 | +| Default 租户 = US 业务库 | host 中 Default 的 `TenantConnectionString` 指向 `antis-foodlabeling-us`;对该租户执行 `company-admin` / `company-role-menus` **会改美版库** | | |
| 166 | +| 新公司必须独立库 | Provision 使用 `antis-foodlabeling-{tenant}` 模板;勿把新公司指到 `us` | | |
| 167 | +| 删除保护 | 不可删 Default;不可 DROP `host`/`us` | | |
| 168 | +| Token 隔离 | 平台 Token 调公司 `my-menus` 会因无租户失败;公司 Token 勿当平台管理凭证 | | |
| 169 | + | |
| 170 | +**建议**:平台运营界面操作公司时,只选择泰额独立业务库租户(如 `中国麦当劳公司` → `antis-foodlabeling-t46d56528`),避免点选 Default。 | |
| 171 | + | |
| 172 | +--- | |
| 173 | + | |
| 174 | +## 六、接口速查(含 curl) | |
| 175 | + | |
| 176 | +Base:`http://127.0.0.1:19002`(以环境为准)。 | |
| 177 | + | |
| 178 | +```bash | |
| 179 | +# 平台登录 | |
| 180 | +curl -X POST "http://127.0.0.1:19002/api/app/account/login" \ | |
| 181 | + -H "Content-Type: application/json" \ | |
| 182 | + -d "{\"userName\":\"admin@example.com\",\"password\":\"123456\"}" | |
| 183 | + | |
| 184 | +# 公司列表(含加密账号密码) | |
| 185 | +curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-list" \ | |
| 186 | + --data-urlencode "skipCount=0" --data-urlencode "maxResultCount=20" \ | |
| 187 | + -H "Authorization: Bearer <platform-token>" | |
| 188 | + | |
| 189 | +# 改公司管理员(password 为前端 AES 密文) | |
| 190 | +curl -X PUT "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-admin" \ | |
| 191 | + -H "Authorization: Bearer <platform-token>" \ | |
| 192 | + -H "Content-Type: application/json" \ | |
| 193 | + -d "{\"tenantId\":\"<guid>\",\"loginAccount\":\"admin\",\"password\":\"<cipher>\",\"passwordSalt\":\"<iv>\"}" | |
| 194 | + | |
| 195 | +# 公司 SaaS 菜单 | |
| 196 | +curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-menus" \ | |
| 197 | + --data-urlencode "tenantId=<guid>" \ | |
| 198 | + -H "Authorization: Bearer <platform-token>" | |
| 199 | + | |
| 200 | +# 公司角色 | |
| 201 | +curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-roles" \ | |
| 202 | + --data-urlencode "tenantId=<guid>" \ | |
| 203 | + -H "Authorization: Bearer <platform-token>" | |
| 204 | +``` | |
| 205 | + | |
| 206 | +公司端自测登录(验证平台改密是否生效): | |
| 207 | + | |
| 208 | +```bash | |
| 209 | +curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \ | |
| 210 | + -H "Content-Type: application/json" \ | |
| 211 | + -d "{\"tenantId\":\"<guid>\",\"userName\":\"admin\",\"password\":\"新明文密码\"}" | |
| 212 | +``` | |
| 213 | + | |
| 214 | +--- | |
| 215 | + | |
| 216 | +## 七、关键代码路径 | |
| 217 | + | |
| 218 | +| 模块 | 路径 | | |
| 219 | +|------|------| | |
| 220 | +| 平台多租户服务 | `FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs` | | |
| 221 | +| 开通租户 | `FoodLabeling.Th.Application/Services/ThTenantProvisioningAppService.cs` | | |
| 222 | +| 业务库直连 | `FoodLabeling.Th.Application/MultiTenancy/TenantBusinessDatabaseAccessor.cs` | | |
| 223 | +| AES 凭据 | `FoodLabeling.Th.Application/MultiTenancy/TenantSelectCredentialCipher.cs` | | |
| 224 | +| SaaS Key 目录 | `FoodLabeling.Th.Application/MultiTenancy/ThSaasMenuPermissionCatalog.cs` | | |
| 225 | +| 主库凭据实体 | `FoodLabeling.Th.Domain/Entities/ThTenantAdminCredentialEntity.cs` | | |
| 226 | +| 主库 SaaS 菜单实体 | `FoodLabeling.Th.Domain/Entities/ThTenantMenuPermissionEntity.cs` | | |
| 227 | +| 公司 Web 登录 | `FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs` | | |
| 228 | +| 平台登录 | `Yi.Framework.Rbac.Application/Services/AccountService.cs` | | |
| 229 | + | |
| 230 | +--- | |
| 231 | + | |
| 232 | +## 八、风险与自检 | |
| 233 | + | |
| 234 | +| 风险 | 应对 | | |
| 235 | +|------|------| | |
| 236 | +| 对 Default 改密 / 改角色 → 写 US | 运营禁操作 Default;删除已有保护,改密无同等硬拦 | | |
| 237 | +| 凭据表可逆 AES | 保护 `TenantSelectCrypto:SecretKey`;仅平台管理员可见列表 | | |
| 238 | +| 开通未写凭据表 | 改密走平台接口后才会与列表回显一致 | | |
| 239 | +| Init 未完成就改 admin | 等「后台初始化完成」或调 `initialize-tenant-database` | | |
| 240 | +| SaaS keys ≠ RBAC 菜单 | 配置完 `company-menus` 后,公司内角色仍需 `company-role-menus` / 公司端角色管理 | | |
| 241 | + | |
| 242 | +自检清单: | |
| 243 | + | |
| 244 | +- [ ] 平台用 `account/login`(host `admin`),公司用 `th-web-auth/login` | |
| 245 | +- [ ] `company-list` 能看到目标公司 Id 与加密凭据 | |
| 246 | +- [ ] `company-admin` 后,公司端可用新密码登录;主库凭据表有对应行 | |
| 247 | +- [ ] `company-menus` 只改 host;`company-role-menus` 只改目标租户库 | |
| 248 | +- [ ] `antis-foodlabeling-us` 的 user/role 数量未因平台种子脚本变化 | |
| 249 | +- [ ] 未对 Default / `database=us` 执行 DROP | |
| 250 | + | |
| 251 | +--- | |
| 252 | + | |
| 253 | +## 九、小结 | |
| 254 | + | |
| 255 | +平台端操作公司级账号的本质是: | |
| 256 | + | |
| 257 | +1. **元数据与回显凭据**放在主库 host; | |
| 258 | +2. **真正能登录的公司账号**在各公司业务库 `User`; | |
| 259 | +3. 平台通过 `tenantId` + 直连连接串跨库改公司 admin / 角色菜单; | |
| 260 | +4. SaaS 模块开关与公司内 RBAC 是两层模型; | |
| 261 | +5. **永远不要把美国版库当成泰额平台种子库或随意操作 Default 租户。** | ... | ... |
项目相关文档/2026-07-23泰额版当前登录账号菜单接口.md
0 → 100644
| 1 | +# 2026-07-23 泰额版:当前登录账号菜单接口 | |
| 2 | + | |
| 3 | +本文档说明 **泰额版**「获取当前登录账号菜单」的两套接口:**平台级**与**公司级(租户)**。 | |
| 4 | + | |
| 5 | +> 与「平台给公司配置 SaaS 菜单权限」(`menu-permission-tree` / `company-menus`)不同: | |
| 6 | +> 本文接口返回的是**当前登录人自己可见的菜单**,用于侧边栏 / 动态路由 / 按钮权限。 | |
| 7 | + | |
| 8 | +--- | |
| 9 | + | |
| 10 | +## 一、对照一览 | |
| 11 | + | |
| 12 | +| 维度 | 平台级 | 公司级(租户业务端) | | |
| 13 | +|------|--------|----------------------| | |
| 14 | +| 适用角色 | 平台管理员(Yi 框架 / 主库账号) | 某公司(租户)下的业务管理员 / 员工 | | |
| 15 | +| 登录接口 | `POST /api/app/account/login` | `POST /api/app/th-web-auth/login`(须带 `tenantId`) | | |
| 16 | +| 拉菜单接口 | `GET /api/app/account/Vue3Router/vben5` | `GET /api/app/auth-session/my-menus` | | |
| 17 | +| 备选 | `GET /api/app/account`(用户+角色+菜单) | — | | |
| 18 | +| 数据来源 | 当前 Account 上下文下的 `menu`(按 `MenuSource` 过滤) | **当前租户业务库** `menu` + 角色菜单 | | |
| 19 | +| 租户上下文 | 一般**无** `__tenant` / JWT TenantId | JWT 含 `TenantId`,建议 Header 再带 `__tenant` | | |
| 20 | +| 实现 | `AccountService.GetVue3Router` | `AuthSessionAppService.GetMyMenusAsync` | | |
| 21 | + | |
| 22 | +**Base URL 示例**:`http://127.0.0.1:19002`(以 `appsettings` / 部署环境为准)。 | |
| 23 | + | |
| 24 | +--- | |
| 25 | + | |
| 26 | +## 二、公司级:当前登录账号菜单 | |
| 27 | + | |
| 28 | +### 2.1 接口 | |
| 29 | + | |
| 30 | +| 项 | 值 | | |
| 31 | +|----|------| | |
| 32 | +| 方法 / 路径 | `GET /api/app/auth-session/my-menus` | | |
| 33 | +| 鉴权 | `Authorization: Bearer {token}` | | |
| 34 | +| 租户 | Token 须来自 `th-web-auth/login`;建议同时传 Header `__tenant: {tenantId}` | | |
| 35 | +| 说明 | 返回当前用户角色、权限码、可见菜单树;未识别租户时会报错引导使用泰额登录 | | |
| 36 | + | |
| 37 | +### 2.2 登录(前置) | |
| 38 | + | |
| 39 | +```http | |
| 40 | +POST /api/app/th-web-auth/login | |
| 41 | +Content-Type: application/json | |
| 42 | + | |
| 43 | +{ | |
| 44 | + "tenantId": "11111111-1111-1111-1111-111111111111", | |
| 45 | + "userName": "admin", | |
| 46 | + "password": "123456" | |
| 47 | +} | |
| 48 | +``` | |
| 49 | + | |
| 50 | +> 不要用 `POST /api/app/account/login` 作为泰额公司端主登录(无租户会查主库)。 | |
| 51 | + | |
| 52 | +### 2.3 curl 示例 | |
| 53 | + | |
| 54 | +```bash | |
| 55 | +# 1) 登录拿 Token | |
| 56 | +curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \ | |
| 57 | + -H "Content-Type: application/json" \ | |
| 58 | + -d "{\"tenantId\":\"<tenantId>\",\"userName\":\"admin\",\"password\":\"123456\"}" | |
| 59 | + | |
| 60 | +# 2) 拉当前账号菜单 | |
| 61 | +curl -G "http://127.0.0.1:19002/api/app/auth-session/my-menus" \ | |
| 62 | + -H "Authorization: Bearer <token>" \ | |
| 63 | + -H "__tenant: <tenantId>" | |
| 64 | +``` | |
| 65 | + | |
| 66 | +### 2.4 出参要点(`CurrentUserMenuPermissionsOutputDto`) | |
| 67 | + | |
| 68 | +| 字段 | 说明 | | |
| 69 | +|------|------| | |
| 70 | +| `user` | 当前用户简要信息 | | |
| 71 | +| `roleCodes` | 角色编码列表 | | |
| 72 | +| `permissionCodes` | 权限码列表(超管常见 `*:*:*`) | | |
| 73 | +| `accessPermissionCodes` | 角色访问权限编码(如 `manage_people`) | | |
| 74 | +| `menus` | 菜单树(`children` 嵌套;根 `parentId` 多为 `"0"`) | | |
| 75 | +| `role` | 角色展示名 | | |
| 76 | +| `fullName` | 全名 | | |
| 77 | +| `lastUpdated` | 系统编辑 / 业务变更相关时间(供前端刷新缓存) | | |
| 78 | + | |
| 79 | +未带租户上下文时,典型错误提示: | |
| 80 | + | |
| 81 | +> 未识别租户上下文。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 `__tenant` 携带租户 Id。 | |
| 82 | + | |
| 83 | +--- | |
| 84 | + | |
| 85 | +## 三、平台级:当前登录账号菜单 | |
| 86 | + | |
| 87 | +### 3.1 接口(推荐) | |
| 88 | + | |
| 89 | +| 项 | 值 | | |
| 90 | +|----|------| | |
| 91 | +| 方法 / 路径 | `GET /api/app/account/Vue3Router/vben5` | | |
| 92 | +| 鉴权 | `Authorization: Bearer {token}` | | |
| 93 | +| 说明 | 将当前登录用户菜单转为 **Vben5** 前端路由结构 | | |
| 94 | + | |
| 95 | +`routerType` 路径参数: | |
| 96 | + | |
| 97 | +| 值 | 对应 `MenuSourceEnum` | 说明 | | |
| 98 | +|----|----------------------|------| | |
| 99 | +| `vben5`(推荐) | 2 = Vben5 | Vben Admin v5 路由构建 | | |
| 100 | +| `ruoyi` | 0 = Ruoyi | 若依 Vue3 路由构建 | | |
| 101 | +| `pure` | 1 = Pure | vue-pure-admin 路由构建 | | |
| 102 | +| 省略 | 默认按 ruoyi | 与框架历史默认一致 | | |
| 103 | + | |
| 104 | +完整路径示例: | |
| 105 | + | |
| 106 | +- `GET /api/app/account/Vue3Router/vben5` | |
| 107 | +- `GET /api/app/account/Vue3Router/ruoyi` | |
| 108 | +- `GET /api/app/account/Vue3Router/pure` | |
| 109 | + | |
| 110 | +### 3.2 备选:账户信息(含菜单) | |
| 111 | + | |
| 112 | +| 项 | 值 | | |
| 113 | +|----|------| | |
| 114 | +| 方法 / 路径 | `GET /api/app/account` | | |
| 115 | +| 鉴权 | 需登录 | | |
| 116 | +| 说明 | 返回 `UserRoleMenuDto`(用户、角色、菜单等),非专门「路由树」格式 | | |
| 117 | + | |
| 118 | +### 3.3 登录(前置) | |
| 119 | + | |
| 120 | +```http | |
| 121 | +POST /api/app/account/login | |
| 122 | +Content-Type: application/json | |
| 123 | + | |
| 124 | +{ | |
| 125 | + "userName": "admin", | |
| 126 | + "password": "123456" | |
| 127 | +} | |
| 128 | +``` | |
| 129 | + | |
| 130 | +> 平台主库账号;一般不传 `tenantId`。字段名以 Swagger 为准(历史上 Web 登录可能按邮箱匹配)。 | |
| 131 | + | |
| 132 | +### 3.4 curl 示例 | |
| 133 | + | |
| 134 | +```bash | |
| 135 | +# 1) 平台登录 | |
| 136 | +curl -X POST "http://127.0.0.1:19002/api/app/account/login" \ | |
| 137 | + -H "Content-Type: application/json" \ | |
| 138 | + -d "{\"userName\":\"admin\",\"password\":\"123456\"}" | |
| 139 | + | |
| 140 | +# 2) 拉当前账号 Vben5 路由菜单 | |
| 141 | +curl -G "http://127.0.0.1:19002/api/app/account/Vue3Router/vben5" \ | |
| 142 | + -H "Authorization: Bearer <platform-token>" | |
| 143 | +``` | |
| 144 | + | |
| 145 | +### 3.5 行为说明 | |
| 146 | + | |
| 147 | +- 按当前用户角色关联菜单过滤;用户名为 `admin` 时框架侧可返回对应 `MenuSource` 下全部菜单再构建路由。 | |
| 148 | +- 只消费 `menu` 表中 `MenuSource` 与 `routerType` 匹配的记录。 | |
| 149 | + | |
| 150 | +--- | |
| 151 | + | |
| 152 | +## 四、容易混淆的接口(不是「当前登录人菜单」) | |
| 153 | + | |
| 154 | +以下属于**平台给公司配置权限**,不要当成「当前登录账号菜单」: | |
| 155 | + | |
| 156 | +| 方法 / 路径 | 用途 | | |
| 157 | +|-------------|------| | |
| 158 | +| `GET /api/app/th-multi-tenancy/menu-permission-tree` | SaaS 菜单权限目录树(配置用) | | |
| 159 | +| `GET /api/app/th-multi-tenancy/company-menus` | 查询某公司已开通的 `menuPermissionKeys` | | |
| 160 | +| `PUT /api/app/th-multi-tenancy/company-menus` | 设置某公司 SaaS 菜单权限 | | |
| 161 | +| `GET /api/app/th-rbac-menu/tree` | 租户业务库菜单 **CRUD/管理** 全量树(非会话权限接口) | | |
| 162 | + | |
| 163 | +详见:`项目相关文档/2026-07-21代码优化.md`、`项目相关文档/2026-07-22代码优化.md`。 | |
| 164 | + | |
| 165 | +--- | |
| 166 | + | |
| 167 | +## 五、前端对接建议 | |
| 168 | + | |
| 169 | +| 端 | 登录后拉菜单 | | |
| 170 | +|----|----------------| | |
| 171 | +| 公司 / 租户 Web | `th-web-auth/login` → `auth-session/my-menus`;请求始终带 Token + `__tenant` | | |
| 172 | +| 平台管理端(Vben5) | `account/login` → `account/Vue3Router/vben5` | | |
| 173 | +| Token 混用 | 禁止:公司 Token 调平台路由,或平台 Token 调 `my-menus`(缺租户会失败) | | |
| 174 | + | |
| 175 | +--- | |
| 176 | + | |
| 177 | +## 六、相关代码位置 | |
| 178 | + | |
| 179 | +| 角色 | 路径 | | |
| 180 | +|------|------| | |
| 181 | +| 公司级菜单 | `food-labeling-us/.../AuthSessionAppService.cs` → `GetMyMenusAsync` | | |
| 182 | +| 公司级登录 | `FoodLabeling.Th.Application/.../ThWebAuthAppService.cs` | | |
| 183 | +| 平台级路由 | `Yi.Framework.Rbac.Application/.../AccountService.cs` → `GetVue3Router` | | |
| 184 | +| 平台级登录 | 同 `AccountService` 登录接口 | | |
| 185 | + | |
| 186 | +--- | |
| 187 | + | |
| 188 | +## 七、自检清单 | |
| 189 | + | |
| 190 | +- [ ] 公司端:用 `th-web-auth/login` 拿到 Token 后再调 `auth-session/my-menus` | |
| 191 | +- [ ] 公司端:Header 含 `__tenant` 或 JWT 内 `TenantId` 有效 | |
| 192 | +- [ ] 平台端:用 `account/login` 后再调 `account/Vue3Router/vben5` | |
| 193 | +- [ ] 未把 `company-menus` / `menu-permission-tree` 误当作当前登录人菜单 | |
| 194 | +- [ ] Swagger 中确认实际 Host / 路径与本文一致 | ... | ... |
项目相关文档/2026-07-23美国版在线批量导入接口.md
0 → 100644
| 1 | +# 2026-07-23 美国版:Region / Location / Team Member / Product 在线批量导入 | |
| 2 | + | |
| 3 | +本文档说明 **美国版**(`food-labeling-us`)四个 **JSON 在线批量导入**接口:前端在系统内表格/表单编辑后一次提交,**不再依赖下载 Excel 模板**(Excel 导入接口仍保留,但不推荐)。 | |
| 4 | + | |
| 5 | +> 汇总表与 Excel/导出/批量编辑说明仍见:`项目相关文档/批量导入导出接口说明.md`。 | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 一、为什么改成在线导入 | |
| 10 | + | |
| 11 | +| 方式 | 问题 / 优势 | | |
| 12 | +|------|-------------| | |
| 13 | +| Excel 下载模板再上传 | 表头别名、列顺序、Region 名称 vs Id 等易填错;模板版本与环境不一致 | | |
| 14 | +| **JSON 在线导入(本文)** | 与单条 `Create` 字段一致;前端下拉选 Company/Region/Location(传 Id);校验即时、失败行可定位 | | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## 二、公共约定 | |
| 19 | + | |
| 20 | +| 项 | 说明 | | |
| 21 | +|----|------| | |
| 22 | +| 宿主 | 美国版 `Yi.Abp.Web`;Swagger 分组「食品标签-美国版接口」 | | |
| 23 | +| 基址示例 | `http://localhost:19001` / 测试环境 `https://flus-test.3ffoodsafety.com` | | |
| 24 | +| Content-Type | `application/json` | | |
| 25 | +| 鉴权 | `Authorization: Bearer {token}`(与其它业务接口相同) | | |
| 26 | +| 请求体 | `{ "items": [ ... ] }`,每元素字段与对应模块**单条新增**一致 | | |
| 27 | +| 行语义 | **仅新增**;批量编辑仍用各模块 `*-bulk`(Update) | | |
| 28 | +| 失败策略 | **逐行**调用现有 `CreateAsync`;部分成功;单行 `UserFriendlyException` 记入 `errors` | | |
| 29 | +| `index` | 失败行在 `items` 中的下标,**从 0 开始** | | |
| 30 | +| 上限 | `FoodLabeling:BatchImport:MaxImportRows`(默认 **5000**) | | |
| 31 | +| Excel 旧接口 | 保留;文档标注不推荐,请优先本文接口 | | |
| 32 | + | |
| 33 | +### 2.1 接口一览 | |
| 34 | + | |
| 35 | +| 模块 | UI 概念 | HTTP | 路径 | | |
| 36 | +|------|---------|------|------| | |
| 37 | +| Group | **Region** | POST | `/api/app/group/batch-import-online` | | |
| 38 | +| Location | Location | POST | `/api/app/location/batch-import-online` | | |
| 39 | +| Team Member | Team Member | POST | `/api/app/team-member/batch-import-online` | | |
| 40 | +| Product | Product | POST | `/api/app/product/batch-import-online` | | |
| 41 | + | |
| 42 | +### 2.2 统一出参结构 | |
| 43 | + | |
| 44 | +```json | |
| 45 | +{ | |
| 46 | + "successCount": 2, | |
| 47 | + "failCount": 1, | |
| 48 | + "errors": [ | |
| 49 | + { | |
| 50 | + "index": 2, | |
| 51 | + "message": "具体业务错误信息", | |
| 52 | + "groupName": "可选,按模块不同字段名见下表" | |
| 53 | + } | |
| 54 | + ] | |
| 55 | +} | |
| 56 | +``` | |
| 57 | + | |
| 58 | +| 模块 | 失败行业务键字段(camelCase) | | |
| 59 | +|------|------------------------------| | |
| 60 | +| Region | `groupName` | | |
| 61 | +| Location | `locationCode` | | |
| 62 | +| Team Member | `userName` | | |
| 63 | +| Product | `productName` | | |
| 64 | + | |
| 65 | +整单失败(HTTP 400):`items` 为空,或条数超过 `MaxImportRows`。 | |
| 66 | + | |
| 67 | +### 2.3 与批量编辑的区别 | |
| 68 | + | |
| 69 | +| | 在线批量导入(本文) | 批量编辑(已有) | | |
| 70 | +|--|---------------------|------------------| | |
| 71 | +| 路径示例 | `.../batch-import-online` | `.../locations-bulk` 等 | | |
| 72 | +| 行语义 | 新建 | 按 `id` 更新 | | |
| 73 | +| 典型场景 | 系统内批量录入新数据 | 网格「保存全部」 | | |
| 74 | + | |
| 75 | +--- | |
| 76 | + | |
| 77 | +## 三、Region(Group)在线导入 | |
| 78 | + | |
| 79 | +| 项 | 值 | | |
| 80 | +|----|------| | |
| 81 | +| 路径 | `POST /api/app/group/batch-import-online` | | |
| 82 | +| 服务 | `GroupAppService.BatchImportOnlineAsync` | | |
| 83 | +| items 元素 | `GroupCreateInputVo` | | |
| 84 | + | |
| 85 | +**字段映射**:UI **Region** = 库表 `fl_group.GroupName`(入参字段名仍为 `groupName`)。 | |
| 86 | + | |
| 87 | +| 字段 | 必填 | 说明 | | |
| 88 | +|------|------|------| | |
| 89 | +| `groupName` | 是 | Region 名称 | | |
| 90 | +| `partnerId` | 是 | 所属 Company(`fl_partner.Id`) | | |
| 91 | +| `state` | 否 | 启用状态,默认 `true` | | |
| 92 | + | |
| 93 | +```bash | |
| 94 | +curl -X POST "http://localhost:19001/api/app/group/batch-import-online" \ | |
| 95 | + -H "Authorization: Bearer <token>" \ | |
| 96 | + -H "Content-Type: application/json" \ | |
| 97 | + -d "{ | |
| 98 | + \"items\": [ | |
| 99 | + { \"groupName\": \"NC Region\", \"partnerId\": \"<partnerGuid>\", \"state\": true }, | |
| 100 | + { \"groupName\": \"SC Region\", \"partnerId\": \"<partnerGuid>\", \"state\": true } | |
| 101 | + ] | |
| 102 | + }" | |
| 103 | +``` | |
| 104 | + | |
| 105 | +权限与单条 `POST /api/app/group` 相同(含 Company Admin 自动绑定门店等逻辑)。 | |
| 106 | + | |
| 107 | +--- | |
| 108 | + | |
| 109 | +## 四、Location 在线导入 | |
| 110 | + | |
| 111 | +| 项 | 值 | | |
| 112 | +|----|------| | |
| 113 | +| 路径 | `POST /api/app/location/batch-import-online` | | |
| 114 | +| 服务 | `LocationAppService.BatchImportOnlineAsync` | | |
| 115 | +| items 元素 | `LocationCreateInputVo` | | |
| 116 | + | |
| 117 | +| 字段 | 必填 | 说明 | | |
| 118 | +|------|------|------| | |
| 119 | +| `locationCode` | 是 | Location ID(业务编码) | | |
| 120 | +| `locationName` | 是 | 门店名称 | | |
| 121 | +| `partner` | 否 | Company **名称**(与单条新增一致,非 Id) | | |
| 122 | +| `groupName` | 否 | Region 名称(对应 `location.GroupName`) | | |
| 123 | +| `street` / `city` / `stateCode` / `country` / `zipCode` | 否 | 地址 | | |
| 124 | +| `phone` / `email` | 否 | 联系方式 | | |
| 125 | +| `latitude` / `longitude` | 否 | 坐标 | | |
| 126 | +| `operatingHours` | 否 | 营业时间文本 | | |
| 127 | +| `state` | 否 | 启用,默认 `true` | | |
| 128 | + | |
| 129 | +```bash | |
| 130 | +curl -X POST "http://localhost:19001/api/app/location/batch-import-online" \ | |
| 131 | + -H "Authorization: Bearer <token>" \ | |
| 132 | + -H "Content-Type: application/json" \ | |
| 133 | + -d "{ | |
| 134 | + \"items\": [ | |
| 135 | + { | |
| 136 | + \"locationCode\": \"LOC001\", | |
| 137 | + \"locationName\": \"UNCC store\", | |
| 138 | + \"partner\": \"MedVantage Cafe Group\", | |
| 139 | + \"groupName\": \"NC Region\", | |
| 140 | + \"city\": \"Charlotte\", | |
| 141 | + \"stateCode\": \"NC\", | |
| 142 | + \"country\": \"USA\", | |
| 143 | + \"state\": true | |
| 144 | + } | |
| 145 | + ] | |
| 146 | + }" | |
| 147 | +``` | |
| 148 | + | |
| 149 | +--- | |
| 150 | + | |
| 151 | +## 五、Team Member 在线导入 | |
| 152 | + | |
| 153 | +| 项 | 值 | | |
| 154 | +|----|------| | |
| 155 | +| 路径 | `POST /api/app/team-member/batch-import-online` | | |
| 156 | +| 服务 | `TeamMemberAppService.BatchImportOnlineAsync` | | |
| 157 | +| items 元素 | `TeamMemberCreateInputVo` | | |
| 158 | + | |
| 159 | +| 字段 | 必填 | 说明 | | |
| 160 | +|------|------|------| | |
| 161 | +| `fullName` | 是 | 姓名 | | |
| 162 | +| `userName` | 是 | 登录账号 | | |
| 163 | +| `password` | 否 | 空则用配置 `TeamMemberImportDefaultPassword` | | |
| 164 | +| `email` / `phone` | 否 | 联系方式 | | |
| 165 | +| `roleId` | 否 | 角色 Guid | | |
| 166 | +| `partnerId` / `partnerIds` | 否 | Company Id | | |
| 167 | +| `regionIds` / `groupIds` | 否 | Region(`fl_group.Id`),**传 Id 不要传名称** | | |
| 168 | +| `locationIds` | 否 | 门店 Id 列表 | | |
| 169 | +| `state` | 否 | 默认 `true` | | |
| 170 | + | |
| 171 | +与 Excel 导入差异:在线接口的 Region/Location 使用 **Id**,由前端下拉选择,避免名称拼写错误。 | |
| 172 | + | |
| 173 | +```bash | |
| 174 | +curl -X POST "http://localhost:19001/api/app/team-member/batch-import-online" \ | |
| 175 | + -H "Authorization: Bearer <token>" \ | |
| 176 | + -H "Content-Type: application/json" \ | |
| 177 | + -d "{ | |
| 178 | + \"items\": [ | |
| 179 | + { | |
| 180 | + \"fullName\": \"John Doe\", | |
| 181 | + \"userName\": \"john@example.com\", | |
| 182 | + \"email\": \"john@example.com\", | |
| 183 | + \"roleId\": \"<roleGuid>\", | |
| 184 | + \"locationIds\": [\"<locationGuid>\"], | |
| 185 | + \"state\": true | |
| 186 | + } | |
| 187 | + ] | |
| 188 | + }" | |
| 189 | +``` | |
| 190 | + | |
| 191 | +--- | |
| 192 | + | |
| 193 | +## 六、Product 在线导入 | |
| 194 | + | |
| 195 | +| 项 | 值 | | |
| 196 | +|----|------| | |
| 197 | +| 路径 | `POST /api/app/product/batch-import-online` | | |
| 198 | +| 服务 | `ProductAppService.BatchImportOnlineAsync` | | |
| 199 | +| items 元素 | `ProductCreateInputVo` | | |
| 200 | + | |
| 201 | +| 字段 | 必填 | 说明 | | |
| 202 | +|------|------|------| | |
| 203 | +| `productName` | 是 | 产品名称 | | |
| 204 | +| `productCode` | 否 | 空则后端生成唯一编码 | | |
| 205 | +| `categoryId` | 否 | 分类 Id | | |
| 206 | +| `productImageUrl` / `displayText` / `codeValue` | 否 | 展示相关 | | |
| 207 | +| `buttonAppearance` / `categoryPhotoUrl` | 否 | 按钮外观 | | |
| 208 | +| `availabilityType` | 否 | `ALL` / `SPECIFIED` | | |
| 209 | +| `partnerId` | 否 | Company Id,展开门店后写入关联 | | |
| 210 | +| `groupIds` | 否 | Region Id 列表 | | |
| 211 | +| `locationIds` | 否 | 门店 Id 列表 | | |
| 212 | +| `state` | 否 | 默认 `true` | | |
| 213 | + | |
| 214 | +```bash | |
| 215 | +curl -X POST "http://localhost:19001/api/app/product/batch-import-online" \ | |
| 216 | + -H "Authorization: Bearer <token>" \ | |
| 217 | + -H "Content-Type: application/json" \ | |
| 218 | + -d "{ | |
| 219 | + \"items\": [ | |
| 220 | + { | |
| 221 | + \"productName\": \"Tuna & Bacon Sub\", | |
| 222 | + \"productCode\": \"40001\", | |
| 223 | + \"categoryId\": \"<categoryId>\", | |
| 224 | + \"state\": true, | |
| 225 | + \"locationIds\": [\"<locationGuid>\"] | |
| 226 | + } | |
| 227 | + ] | |
| 228 | + }" | |
| 229 | +``` | |
| 230 | + | |
| 231 | +--- | |
| 232 | + | |
| 233 | +## 七、配置 | |
| 234 | + | |
| 235 | +`appsettings` 节:`FoodLabeling:BatchImport` | |
| 236 | + | |
| 237 | +| 配置项 | 与在线导入相关 | | |
| 238 | +|--------|----------------| | |
| 239 | +| `MaxImportRows` | 单次 `items` 最大条数(默认 5000) | | |
| 240 | +| `TeamMemberImportDefaultPassword` | Team Member 未填密码时的默认初始密码 | | |
| 241 | + | |
| 242 | +--- | |
| 243 | + | |
| 244 | +## 八、前端对接建议 | |
| 245 | + | |
| 246 | +1. 批量录入页用系统内表格 + 下拉(Company / Region / Location / Role),提交 JSON,**不要**再引导用户下载 Excel 模板。 | |
| 247 | +2. 展示返回的 `successCount` / `failCount`,按 `errors[].index` 高亮失败行。 | |
| 248 | +3. Region 展示文案用「Region」,请求字段仍用 `groupName` / `partnerId`。 | |
| 249 | +4. 批量改已有数据继续走各模块 **bulk update**,勿用本导入接口。 | |
| 250 | +5. 部署后需重启后端;Swagger 中确认路径为带模块前缀的 `.../group|location|team-member|product/batch-import-online`。 | |
| 251 | + | |
| 252 | +--- | |
| 253 | + | |
| 254 | +## 九、相关代码 | |
| 255 | + | |
| 256 | +| 类型 | 路径 | | |
| 257 | +|------|------| | |
| 258 | +| Group | `FoodLabeling.Application/Services/GroupAppService.cs` | | |
| 259 | +| Location | `FoodLabeling.Application/Services/LocationAppService.cs` | | |
| 260 | +| Team Member | `FoodLabeling.Application/Services/TeamMemberAppService.cs` | | |
| 261 | +| Product | `FoodLabeling.Application/Services/ProductAppService.cs` | | |
| 262 | +| DTO | `FoodLabeling.Application.Contracts/Dtos/{Group,Location,TeamMember,Product}/*BatchImportOnline*.cs` | | |
| 263 | + | |
| 264 | +模块根目录:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/`。 | |
| 265 | + | |
| 266 | +--- | |
| 267 | + | |
| 268 | +## 十、自检清单 | |
| 269 | + | |
| 270 | +- [ ] 平台/公司账号登录后 Token 可调业务接口 | |
| 271 | +- [ ] Region:合法 `partnerId` + `groupName` 可批量成功 | |
| 272 | +- [ ] Location:重复 `locationCode` 记入 `errors`,其它行仍成功 | |
| 273 | +- [ ] Team Member:不传 `password` 时使用默认密码可登录 | |
| 274 | +- [ ] Product:不传 `productCode` 时后端自动生成 | |
| 275 | +- [ ] `items` 超过 `MaxImportRows` 返回 400 | |
| 276 | +- [ ] 未误用 Excel 导入作为新前端主路径 | ... | ... |
项目相关文档/2026-07-24代码优化.md
0 → 100644
| 1 | +# 2026-07-24 Team Member 范围 ALL 哨兵(美国版后端) | |
| 2 | + | |
| 3 | +## 背景 | |
| 4 | + | |
| 5 | +New User / Edit User 弹窗中 Locations、Region 支持勾选 **ALL**。前端可传 `locationIds: ["ALL"]`(或 `regionIds: ["ALL"]`),后端需按当前 Company(`partnerId` / `partnerIds`)展开并落库到 `userlocation`;列表 **Assigned Locations** 覆盖全部门店时展示 **`All Location`**,Region 覆盖全部时展示 **`All Region`**。 | |
| 6 | + | |
| 7 | +本次**仅改美国版后端**,不改 Web 前端。 | |
| 8 | + | |
| 9 | +--- | |
| 10 | + | |
| 11 | +## 接口 | |
| 12 | + | |
| 13 | +| 方法 | 路径 | 说明 | | |
| 14 | +|------|------|------| | |
| 15 | +| POST | `/api/app/team-member` | 新增成员 | | |
| 16 | +| PUT | `/api/app/team-member/{id}` | 更新成员 | | |
| 17 | + | |
| 18 | +批量编辑 `PUT /api/app/team-member/team-members-bulk` 单行字段与单条 PUT 相同,同样支持 ALL。 | |
| 19 | + | |
| 20 | +--- | |
| 21 | + | |
| 22 | +## ALL 传参约定 | |
| 23 | + | |
| 24 | +### 哨兵值 | |
| 25 | + | |
| 26 | +- 字符串 **`ALL`**(大小写不敏感:`ALL` / `all` / `All`) | |
| 27 | +- 适用字段:`locationIds`、`regionIds`、`groupIds`(与 `regionIds` 等价)、`locations`(与 `locationIds` 合并解析) | |
| 28 | + | |
| 29 | +### 与具体 Guid 同传 | |
| 30 | + | |
| 31 | +若数组中同时含 `ALL` 与具体门店/Region Guid,**以 ALL 为准**(视为全选,忽略具体 Id)。 | |
| 32 | + | |
| 33 | +### 优先级(Team Member 落库) | |
| 34 | + | |
| 35 | +**不再做 partner + region + location 并集**;按下列顺序取**单一**规则展开: | |
| 36 | + | |
| 37 | +1. **`locationIds` 含 `ALL`** → 按 `partnerId` / `partnerIds` 展开该公司**全部门店** | |
| 38 | +2. **有具体 `locationIds`,且 `regionIds` 为空或为 `ALL`** → **只绑这些门店**(Region=ALL 下再收窄到指定门店;避免 `regionIds:["ALL"]` 盖掉单店) | |
| 39 | +3. **`regionIds` 含 `ALL`**(且无具体门店)→ 按 Company 展开该公司**全部 Region** 下门店 | |
| 40 | +4. **`regionIds` 有具体 Guid** → **按这些 Region 展开门店落库**(**忽略同传的 `locationIds`**,保证多区域能落库回显) | |
| 41 | +5. **仅有具体 `locationIds`** → 只绑定指定门店 | |
| 42 | +6. **仅有 `partnerId` / `partnerIds`**(Company Admin 等)→ 绑定该公司全部门店 | |
| 43 | + | |
| 44 | +| 请求示例 | 落库规则 | | |
| 45 | +|----------|----------| | |
| 46 | +| `partnerId` + `locationIds: ["ALL"]` | 展开该公司**全部**门店写入 `userlocation` | | |
| 47 | +| `partnerId` + `regionIds: ["ALL"]` + `locationIds: ["{loc}"]` | **只绑 `{loc}`**(Region ALL + 具体门店时门店优先) | | |
| 48 | +| `partnerId` + `regionIds: ["ALL"]`(无 location) | 展开该公司**全部 Region** 下门店 | | |
| 49 | +| `partnerId` + `regionIds: ["{groupGuid1}", "{groupGuid2}"]` | 展开两 Region 下门店并集 | | |
| 50 | +| `partnerId` + `regionIds: ["{g1}","{g2}"]` + `locationIds: ["{loc}"]` | **按 Region 展开**(忽略 `{loc}`),`userlocation` 为两 Region 门店并集;Get 回显 `regionIds` 含 2 个 | | |
| 51 | +| `partnerId` + `locationIds: ["{guid1}", "{guid2}"]`(无 region) | 仅绑定指定门店 | | |
| 52 | +| Company Admin 仅传 `partnerId`(无 region/location) | 仍自动绑定该公司全部门店(原逻辑) | | |
| 53 | + | |
| 54 | +**注意**:`locationIds` / `regionIds` 含 ALL 时须传 **`partnerId` 或 `partnerIds`**,否则返回 400。 | |
| 55 | + | |
| 56 | +### 编辑回显(GET 折叠为 ALL) | |
| 57 | + | |
| 58 | +库表 `userlocation` **仍存展开后的门店 Guid**。`GET /api/app/team-member/{id}`(及列表同名字段)在判断为「已覆盖该公司全部」时,将 Id 数组折叠为哨兵,与新增传 ALL 对称: | |
| 59 | + | |
| 60 | +| 条件 | 出参 | | |
| 61 | +|------|------| | |
| 62 | +| 绑定门店 = 该公司全部门店 | `locationIds: ["ALL"]`,`assignedLocations` 展示 All Location | | |
| 63 | +| 反推 Region = 该公司全部 Region | `regionIds` / `groupIds: ["ALL"]` | | |
| 64 | +| 未全选 | 仍返回具体 Guid 数组 | | |
| 65 | + | |
| 66 | +示例(新增时传 `regionIds/locationIds: ["ALL"]` 后,再 GET): | |
| 67 | + | |
| 68 | +```json | |
| 69 | +{ | |
| 70 | + "partnerIds": ["3a22a48c-9758-9a26-e4ee-390b0f042835"], | |
| 71 | + "regionIds": ["ALL"], | |
| 72 | + "groupIds": ["ALL"], | |
| 73 | + "locationIds": ["ALL"] | |
| 74 | +} | |
| 75 | +``` | |
| 76 | + | |
| 77 | +--- | |
| 78 | + | |
| 79 | +## 请求 / 响应示例 | |
| 80 | + | |
| 81 | +### 1. Locations 全选(ALL) | |
| 82 | + | |
| 83 | +**请求** | |
| 84 | + | |
| 85 | +```json | |
| 86 | +POST /api/app/team-member | |
| 87 | +{ | |
| 88 | + "fullName": "Test All Loc", | |
| 89 | + "userName": "test.all.loc@example.com", | |
| 90 | + "password": "Pass123!", | |
| 91 | + "email": "test.all.loc@example.com", | |
| 92 | + "roleId": "ROLE_GUID", | |
| 93 | + "partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e", | |
| 94 | + "locationIds": ["ALL"], | |
| 95 | + "state": true | |
| 96 | +} | |
| 97 | +``` | |
| 98 | + | |
| 99 | +**落库**:`userlocation` 写入 Partner `Subway USA` 下全部未删除门店 Id。 | |
| 100 | + | |
| 101 | +**列表/详情 `assignedLocations`**(覆盖全部门店时): | |
| 102 | + | |
| 103 | +```json | |
| 104 | +"assignedLocations": [ | |
| 105 | + { "locationName": "All Location" } | |
| 106 | +] | |
| 107 | +``` | |
| 108 | + | |
| 109 | +### 2. 仅指定门店 Guid(对比) | |
| 110 | + | |
| 111 | +**请求** | |
| 112 | + | |
| 113 | +```json | |
| 114 | +"partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e", | |
| 115 | +"locationIds": [ | |
| 116 | + "3a222878-b4c1-d27b-d437-93da14886350", | |
| 117 | + "3a22287c-d353-da47-869f-0bb5dacced51" | |
| 118 | +] | |
| 119 | +``` | |
| 120 | + | |
| 121 | +**列表 `assignedLocations`**:展示具体名称,如 `Subway LAX Store`、`Subway UNCC Store`,**不会**出现 `All Location`。 | |
| 122 | + | |
| 123 | +### 3. Region 全选(ALL) | |
| 124 | + | |
| 125 | +**请求** | |
| 126 | + | |
| 127 | +```json | |
| 128 | +"partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e", | |
| 129 | +"regionIds": ["ALL"] | |
| 130 | +``` | |
| 131 | + | |
| 132 | +**落库**:该公司下所有 Region 对应门店写入 `userlocation`。 | |
| 133 | + | |
| 134 | +**Region 展示**:绑定覆盖该公司全部门店时,列表 Region 文案为 **`All Region`**(`FoodLabelingDisplayConsts.AllRegion`)。 | |
| 135 | + | |
| 136 | +### 4. 编辑多 Region + 同传 location(按 Region 展开) | |
| 137 | + | |
| 138 | +**场景**:UI 勾选 2 个 Region,请求体可能仍带 1 个 `locationIds`(历史选中项)。后端**以 Region 为准**展开,保证多区域能落库并回显。 | |
| 139 | + | |
| 140 | +**请求** | |
| 141 | + | |
| 142 | +```json | |
| 143 | +PUT /api/app/team-member/{id} | |
| 144 | +{ | |
| 145 | + "fullName": "张三", | |
| 146 | + "userName": "123", | |
| 147 | + "email": "123@qq.com", | |
| 148 | + "roleId": "3a21836a-cc04-1783-b054-f2ff3c45d4bf", | |
| 149 | + "partnerId": "3a22a48c-9758-9a26-e4ee-390b0f042835", | |
| 150 | + "regionIds": [ | |
| 151 | + "3a22a48d-98be-7d54-7b9d-6062afb176be", | |
| 152 | + "3a22a48d-5356-8754-d839-a6b7aaad02f9" | |
| 153 | + ], | |
| 154 | + "groupIds": [ | |
| 155 | + "3a22a48d-98be-7d54-7b9d-6062afb176be", | |
| 156 | + "3a22a48d-5356-8754-d839-a6b7aaad02f9" | |
| 157 | + ], | |
| 158 | + "locationIds": ["3a22a48f-4114-ad64-11bf-9adb09f999b9"], | |
| 159 | + "state": true | |
| 160 | +} | |
| 161 | +``` | |
| 162 | + | |
| 163 | +**落库**:`userlocation` 写入两 Region 下门店并集(**不是**仅 `{locationIds}` 那一店)。 | |
| 164 | + | |
| 165 | +**Get 回显**:`regionIds` 含上述 2 个 Region(由 `userlocation` → 门店 → `fl_group` 反推)。 | |
| 166 | + | |
| 167 | +--- | |
| 168 | + | |
| 169 | +## 列表展示规则 | |
| 170 | + | |
| 171 | +| 条件 | Assigned Locations | Region(列表/PDF) | | |
| 172 | +|------|-------------------|-------------------| | |
| 173 | +| `userlocation` 覆盖该成员 Company 下**全部门店** | `All Location` | 全部 Region 时 `All Region` | | |
| 174 | +| 仅部分门店 | 门店 Code/Name,分号拼接 | 具体 Region 名称 | | |
| 175 | + | |
| 176 | +实现位置: | |
| 177 | + | |
| 178 | +- 落库展开:`LocationScopeBindingHelper.ResolveTeamMemberLocationIdsForSaveAsync` | |
| 179 | +- 展示折叠:`TeamMemberScopeDisplayHelper.FormatAssignedLocationsForListAsync` / `FormatRegionTextForListAsync` | |
| 180 | +- 全量回显:`TeamMemberAppService.ApplyCompanyAdminDisplayScopeAsync`(绑定覆盖全部门店时展开 Region) | |
| 181 | + | |
| 182 | +--- | |
| 183 | + | |
| 184 | +## curl 示例 | |
| 185 | + | |
| 186 | +### 获取 Token | |
| 187 | + | |
| 188 | +```bash | |
| 189 | +curl -X POST "http://localhost:19001/api/oauth/Login" \ | |
| 190 | + -H "Content-Type: application/x-www-form-urlencoded" \ | |
| 191 | + -d "userName=admin&password=123456" | |
| 192 | +``` | |
| 193 | + | |
| 194 | +将响应中 `data.token`(已含 `Bearer ` 前缀)用于后续 `Authorization` 头。 | |
| 195 | + | |
| 196 | +### 新增 — Locations ALL | |
| 197 | + | |
| 198 | +```bash | |
| 199 | +curl -X POST "http://localhost:19001/api/app/team-member" \ | |
| 200 | + -H "Authorization: <data.token>" \ | |
| 201 | + -H "Content-Type: application/json" \ | |
| 202 | + -d "{ | |
| 203 | + \"fullName\": \"All Loc User\", | |
| 204 | + \"userName\": \"all.loc.user@example.com\", | |
| 205 | + \"password\": \"Pass123!\", | |
| 206 | + \"email\": \"all.loc.user@example.com\", | |
| 207 | + \"roleId\": \"<ROLE_GUID>\", | |
| 208 | + \"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\", | |
| 209 | + \"locationIds\": [\"ALL\"], | |
| 210 | + \"state\": true | |
| 211 | + }" | |
| 212 | +``` | |
| 213 | + | |
| 214 | +### 更新 — Region ALL | |
| 215 | + | |
| 216 | +```bash | |
| 217 | +curl -X PUT "http://localhost:19001/api/app/team-member/<USER_GUID>" \ | |
| 218 | + -H "Authorization: <data.token>" \ | |
| 219 | + -H "Content-Type: application/json" \ | |
| 220 | + -d "{ | |
| 221 | + \"fullName\": \"All Region User\", | |
| 222 | + \"userName\": \"all.region.user@example.com\", | |
| 223 | + \"roleId\": \"<ROLE_GUID>\", | |
| 224 | + \"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\", | |
| 225 | + \"regionIds\": [\"ALL\"], | |
| 226 | + \"state\": true | |
| 227 | + }" | |
| 228 | +``` | |
| 229 | + | |
| 230 | +### 查询列表验证 Assigned Locations | |
| 231 | + | |
| 232 | +```bash | |
| 233 | +curl -X GET "http://localhost:19001/api/app/team-member?SkipCount=0&MaxResultCount=20" \ | |
| 234 | + -H "Authorization: <data.token>" | |
| 235 | +``` | |
| 236 | + | |
| 237 | +检查 `items[].assignedLocations` 与 `items[].locationIds`。 | |
| 238 | + | |
| 239 | +--- | |
| 240 | + | |
| 241 | +## 改动文件 | |
| 242 | + | |
| 243 | +| 文件 | 变更 | | |
| 244 | +|------|------| | |
| 245 | +| `FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs` | ALL 哨兵识别;`ResolveTeamMemberLocationIdsForSaveAsync` 展开逻辑 | | |
| 246 | +| `FoodLabeling.Application/Services/TeamMemberAppService.cs` | 合并 `locations`;Create/Update XML 注释;全量绑定展示 | | |
| 247 | +| `FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs` | 字段注释 + `Locations` | | |
| 248 | +| `FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs` | 字段注释 + `Locations` | | |
| 249 | +| `FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs` | Create/Update 接口说明 | | |
| 250 | + | |
| 251 | +--- | |
| 252 | + | |
| 253 | +## 验证 | |
| 254 | + | |
| 255 | +- `dotnet build` 编译通过 | |
| 256 | +- 可选:MCP `user-antis-foodlabeling-us-mysql` 查 `userlocation` 确认 ALL 请求后行数等于该公司 `location` 未删除门店数 | |
| 257 | + | |
| 258 | +--- | |
| 259 | + | |
| 260 | +## label-template 多公司 / 多 Region / 多门店传参与落库(美国版) | |
| 261 | + | |
| 262 | +### 背景 | |
| 263 | + | |
| 264 | +`POST/PUT /api/app/label-template` 在 **Company / Region / Location** 三维均为 `SPECIFIED` 且各传多个 Id 时,若所选 Id 恰好等于「当前上下文全部可选项」,旧逻辑会把维度折叠为 `ALL` 并**清空关联表**,详情再按门店反推 Company,常表现为 **只回显 1 个公司**。 | |
| 265 | + | |
| 266 | +### 根因 | |
| 267 | + | |
| 268 | +`AllScopeBindingHelper.ResolveDimensionType` 在 `isFullSelection == true` 时一律返回 `ALL`,**未尊重入参显式 `appliedPartnerType` / `appliedRegionType` / `appliedLocation` = `SPECIFIED`**。 | |
| 269 | + | |
| 270 | +### 修复约定 | |
| 271 | + | |
| 272 | +| 入参 | 落库行为 | | |
| 273 | +|------|----------| | |
| 274 | +| `appliedPartnerType: "SPECIFIED"` + `partnerIds` / `companyIds` 数组 | **始终**写 `fl_label_template_partner`(每个 Id 一行),主表 `AppliedPartnerType = SPECIFIED` | | |
| 275 | +| `appliedRegionType: "SPECIFIED"` + `regionIds` / `groupIds` | **始终**写 `fl_label_template_region` | | |
| 276 | +| `appliedLocation: "SPECIFIED"` + `locationIds` / `appliedLocationIds` | **始终**写 `fl_label_template_location` | | |
| 277 | +| 各维度 `ALL` 或未显式 `SPECIFIED` 且 Id 覆盖当前上下文全集 | 仍归档为 `ALL`,不写该维度关联快照(后续新增主数据动态适用) | | |
| 278 | + | |
| 279 | +**字段别名(合并去重)** | |
| 280 | + | |
| 281 | +- Company:`partnerIds` ≡ `companyIds`(`fl_partner.Id`) | |
| 282 | +- Region:`regionIds` ≡ `groupIds`(`fl_group.Id`) | |
| 283 | +- Location:`locationIds` ≡ `appliedLocationIds`(`location.Id`) | |
| 284 | + | |
| 285 | +### 请求示例(2 个 Company + 2 个 Region + 2 个门店) | |
| 286 | + | |
| 287 | +```json | |
| 288 | +PUT /api/app/label-template/tpl_xxx | |
| 289 | +{ | |
| 290 | + "appliedPartnerType": "SPECIFIED", | |
| 291 | + "companyIds": ["{partnerId1}", "{partnerId2}"], | |
| 292 | + "partnerIds": ["{partnerId1}", "{partnerId2}"], | |
| 293 | + "appliedRegionType": "SPECIFIED", | |
| 294 | + "regionIds": ["{groupId1}", "{groupId2}"], | |
| 295 | + "groupIds": ["{groupId1}", "{groupId2}"], | |
| 296 | + "appliedLocation": "SPECIFIED", | |
| 297 | + "locationIds": ["{locId1}", "{locId2}"], | |
| 298 | + "appliedLocationIds": ["{locId1}", "{locId2}"] | |
| 299 | +} | |
| 300 | +``` | |
| 301 | + | |
| 302 | +### 验证(MCP / curl) | |
| 303 | + | |
| 304 | +1. 保存后查库: | |
| 305 | + | |
| 306 | +```sql | |
| 307 | +SELECT AppliedPartnerType, AppliedRegionType, AppliedLocationType | |
| 308 | +FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'; | |
| 309 | + | |
| 310 | +SELECT PartnerId FROM fl_label_template_partner | |
| 311 | +WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'); | |
| 312 | + | |
| 313 | +SELECT GroupId FROM fl_label_template_region | |
| 314 | +WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'); | |
| 315 | + | |
| 316 | +SELECT LocationId FROM fl_label_template_location | |
| 317 | +WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'); | |
| 318 | +``` | |
| 319 | + | |
| 320 | +2. `GET /api/app/label-template/tpl_xxx` 出参 `companyIds` / `partnerIds` / `regionIds` / `locationIds` 数组长度应与入参一致。 | |
| 321 | + | |
| 322 | +### 改动文件 | |
| 323 | + | |
| 324 | +| 文件 | 变更 | | |
| 325 | +|------|------| | |
| 326 | +| `Helpers/AllScopeBindingHelper.cs` | 显式 `SPECIFIED` 时不因全选折叠为 `ALL` | | |
| 327 | +| `Helpers/LabelTemplateScopeHelper.cs` | 保存传参对齐 Label 实体;详情/列表读取主表 scope 类型,避免门店反推覆盖 | | |
| 328 | +| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 批量读取 `AppliedPartnerType` / `AppliedRegionType` | | ... | ... |
项目相关文档/th-platform-user-role-seed.sql
0 → 100644
| 1 | +-- 泰额版平台主库 User / Role / UserRole / RoleMenu 初始化 | |
| 2 | +-- 执行库(必须):antis-foodlabeling-host | |
| 3 | +-- 禁止执行库:antis-foodlabeling-us(美国版,写入会污染) | |
| 4 | +-- 前置:已执行 th-tenant-menu-seed.sql(主库已有 menu) | |
| 5 | +-- 默认账号:admin / 123456(密码哈希与框架种子一致) | |
| 6 | +-- | |
| 7 | +-- ========== 固定 Guid 对照表 ========== | |
| 8 | +-- 平台超级管理员角色 f0020002-0001-4000-8000-000000000002 | |
| 9 | +-- 平台 admin 用户 f0020001-0001-4000-8000-000000000001 | |
| 10 | +-- 用户-角色关系 f0020003-0001-4000-8000-000000000003 | |
| 11 | +-- 角色-菜单关系 f0030001~f0030026(见下方 INSERT) | |
| 12 | +-- ====================================== | |
| 13 | + | |
| 14 | +CREATE TABLE IF NOT EXISTS `user` ( | |
| 15 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 16 | + `IsDeleted` tinyint(1) NOT NULL COMMENT '逻辑删除', | |
| 17 | + `Name` varchar(255) DEFAULT NULL COMMENT '姓名', | |
| 18 | + `Age` int(11) DEFAULT NULL COMMENT '年龄', | |
| 19 | + `UserName` varchar(255) NOT NULL COMMENT '用户名', | |
| 20 | + `Password` varchar(255) NOT NULL COMMENT '密码', | |
| 21 | + `Salt` varchar(255) NOT NULL COMMENT '加密盐值', | |
| 22 | + `Icon` varchar(255) DEFAULT NULL COMMENT '头像', | |
| 23 | + `Nick` varchar(255) DEFAULT NULL COMMENT '昵称', | |
| 24 | + `Email` varchar(255) DEFAULT NULL COMMENT '邮箱', | |
| 25 | + `Ip` varchar(255) DEFAULT NULL COMMENT 'Ip', | |
| 26 | + `Address` varchar(255) DEFAULT NULL COMMENT '地址', | |
| 27 | + `Phone` bigint(20) DEFAULT NULL COMMENT '电话', | |
| 28 | + `Introduction` varchar(255) DEFAULT NULL COMMENT '简介', | |
| 29 | + `Remark` varchar(255) DEFAULT NULL COMMENT '备注', | |
| 30 | + `Sex` int(11) NOT NULL COMMENT '性别', | |
| 31 | + `DeptId` varchar(36) DEFAULT NULL COMMENT '部门id', | |
| 32 | + `CreationTime` datetime NOT NULL COMMENT '创建时间', | |
| 33 | + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', | |
| 34 | + `LastModifierId` varchar(36) DEFAULT NULL COMMENT '最后修改者', | |
| 35 | + `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间', | |
| 36 | + `OrderNum` int(11) NOT NULL COMMENT '排序', | |
| 37 | + `State` tinyint(1) NOT NULL COMMENT '状态', | |
| 38 | + `ConcurrencyStamp` varchar(255) NOT NULL, | |
| 39 | + PRIMARY KEY (`Id`), | |
| 40 | + KEY `index_UserName` (`UserName`) | |
| 41 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表-泰额平台主库'; | |
| 42 | + | |
| 43 | +CREATE TABLE IF NOT EXISTS `role` ( | |
| 44 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 45 | + `IsDeleted` tinyint(1) NOT NULL COMMENT '逻辑删除', | |
| 46 | + `CreationTime` datetime NOT NULL COMMENT '创建时间', | |
| 47 | + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', | |
| 48 | + `LastModifierId` varchar(36) DEFAULT NULL COMMENT '最后修改者', | |
| 49 | + `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间', | |
| 50 | + `OrderNum` int(11) NOT NULL COMMENT '排序', | |
| 51 | + `RoleName` varchar(255) NOT NULL COMMENT '角色名', | |
| 52 | + `RoleCode` varchar(255) NOT NULL COMMENT '角色编码', | |
| 53 | + `Remark` varchar(255) DEFAULT NULL COMMENT '描述', | |
| 54 | + `DataScope` int(11) NOT NULL COMMENT '角色数据范围', | |
| 55 | + `State` tinyint(1) NOT NULL COMMENT '状态', | |
| 56 | + `AccessPermissionCodes` varchar(2000) DEFAULT NULL COMMENT '访问权限 JSON', | |
| 57 | + `ConcurrencyStamp` varchar(255) NOT NULL, | |
| 58 | + PRIMARY KEY (`Id`) | |
| 59 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表-泰额平台主库'; | |
| 60 | + | |
| 61 | +CREATE TABLE IF NOT EXISTS `userrole` ( | |
| 62 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 63 | + `RoleId` varchar(36) NOT NULL COMMENT '角色id', | |
| 64 | + `UserId` varchar(36) NOT NULL COMMENT '用户id', | |
| 65 | + PRIMARY KEY (`Id`) | |
| 66 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关系表-泰额平台主库'; | |
| 67 | + | |
| 68 | +CREATE TABLE IF NOT EXISTS `rolemenu` ( | |
| 69 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 70 | + `RoleId` varchar(36) NOT NULL, | |
| 71 | + `MenuId` varchar(36) NOT NULL, | |
| 72 | + PRIMARY KEY (`Id`) | |
| 73 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关系表-泰额平台主库'; | |
| 74 | + | |
| 75 | +-- 物理清除旧非法 Id,避免脏数据残留 | |
| 76 | +DELETE FROM `userrole` WHERE `Id` LIKE 'th-platform%' OR `UserId` LIKE 'th-platform%' OR `RoleId` LIKE 'th-platform%'; | |
| 77 | +DELETE FROM `rolemenu` WHERE `RoleId` LIKE 'th-platform%' OR `Id` LIKE 'th-prm-%' OR `Id` LIKE 'th-platform%'; | |
| 78 | +DELETE FROM `user` WHERE `Id` LIKE 'th-platform%'; | |
| 79 | +DELETE FROM `role` WHERE `Id` LIKE 'th-platform%'; | |
| 80 | + | |
| 81 | +INSERT INTO `role` | |
| 82 | +(`Id`, `IsDeleted`, `CreationTime`, `CreatorId`, `LastModifierId`, `LastModificationTime`, | |
| 83 | + `OrderNum`, `RoleName`, `RoleCode`, `Remark`, `DataScope`, `State`, `AccessPermissionCodes`, `ConcurrencyStamp`) | |
| 84 | +VALUES | |
| 85 | +('f0020002-0001-4000-8000-000000000002', 0, NOW(), NULL, NULL, NULL, | |
| 86 | + 999, '平台超级管理员', 'admin', '泰额平台主库超级管理员', 0, 1, NULL, UUID()) | |
| 87 | +ON DUPLICATE KEY UPDATE | |
| 88 | + `IsDeleted`=0, | |
| 89 | + `LastModificationTime`=NOW(), | |
| 90 | + `RoleName`=VALUES(`RoleName`), | |
| 91 | + `RoleCode`=VALUES(`RoleCode`), | |
| 92 | + `Remark`=VALUES(`Remark`), | |
| 93 | + `DataScope`=VALUES(`DataScope`), | |
| 94 | + `State`=1, | |
| 95 | + `ConcurrencyStamp`=UUID(); | |
| 96 | + | |
| 97 | +INSERT INTO `user` | |
| 98 | +(`Id`, `IsDeleted`, `Name`, `Age`, `UserName`, `Password`, `Salt`, `Icon`, `Nick`, `Email`, | |
| 99 | + `Ip`, `Address`, `Phone`, `Introduction`, `Remark`, `Sex`, `DeptId`, | |
| 100 | + `CreationTime`, `CreatorId`, `LastModifierId`, `LastModificationTime`, | |
| 101 | + `OrderNum`, `State`, `ConcurrencyStamp`) | |
| 102 | +VALUES | |
| 103 | +('f0020001-0001-4000-8000-000000000001', 0, '超级管理员', 20, 'admin', | |
| 104 | + 'ANg9hGZC3CMBZsfyTE8-AHmP7A30DrI85LxR1btI4HjNwk-wilA9y2-_L4y_kTS14Qq8uIVOeXdYggrShfvNsQ', | |
| 105 | + 'EMXTQOSLmpRmBGS4tMz4mw==', | |
| 106 | + NULL, '超级管理员', 'admin@example.com', | |
| 107 | + NULL, '成都', 13800000000, '泰额平台超级管理员', '平台主库', 0, NULL, | |
| 108 | + NOW(), NULL, NULL, NULL, | |
| 109 | + 999, 1, UUID()) | |
| 110 | +ON DUPLICATE KEY UPDATE | |
| 111 | + `IsDeleted`=0, | |
| 112 | + `LastModificationTime`=NOW(), | |
| 113 | + `Name`=VALUES(`Name`), | |
| 114 | + `Nick`=VALUES(`Nick`), | |
| 115 | + `Email`=VALUES(`Email`), | |
| 116 | + `Password`=VALUES(`Password`), | |
| 117 | + `Salt`=VALUES(`Salt`), | |
| 118 | + `State`=1, | |
| 119 | + `ConcurrencyStamp`=UUID(); | |
| 120 | + | |
| 121 | +INSERT INTO `userrole` (`Id`, `RoleId`, `UserId`) | |
| 122 | +VALUES ('f0020003-0001-4000-8000-000000000003', 'f0020002-0001-4000-8000-000000000002', 'f0020001-0001-4000-8000-000000000001') | |
| 123 | +ON DUPLICATE KEY UPDATE | |
| 124 | + `RoleId`=VALUES(`RoleId`), | |
| 125 | + `UserId`=VALUES(`UserId`); | |
| 126 | + | |
| 127 | +DELETE FROM `rolemenu` WHERE `RoleId` = 'f0020002-0001-4000-8000-000000000002'; | |
| 128 | + | |
| 129 | +INSERT INTO `rolemenu` (`Id`, `RoleId`, `MenuId`) | |
| 130 | +VALUES | |
| 131 | +('f0030001-0001-4000-8000-000000000001', 'f0020002-0001-4000-8000-000000000002', 'f0010001-0001-4000-8000-000000000001'), | |
| 132 | +('f0030002-0001-4000-8000-000000000002', 'f0020002-0001-4000-8000-000000000002', 'f0010002-0001-4000-8000-000000000002'), | |
| 133 | +('f0030003-0001-4000-8000-000000000003', 'f0020002-0001-4000-8000-000000000002', 'f0010003-0001-4000-8000-000000000003'), | |
| 134 | +('f0030004-0001-4000-8000-000000000004', 'f0020002-0001-4000-8000-000000000002', 'f0010010-0001-4000-8000-000000000010'), | |
| 135 | +('f0030005-0001-4000-8000-000000000005', 'f0020002-0001-4000-8000-000000000002', 'f0010011-0001-4000-8000-000000000011'), | |
| 136 | +('f0030006-0001-4000-8000-000000000006', 'f0020002-0001-4000-8000-000000000002', 'f0010012-0001-4000-8000-000000000012'), | |
| 137 | +('f0030007-0001-4000-8000-000000000007', 'f0020002-0001-4000-8000-000000000002', 'f0010013-0001-4000-8000-000000000013'), | |
| 138 | +('f0030008-0001-4000-8000-000000000008', 'f0020002-0001-4000-8000-000000000002', 'f0010014-0001-4000-8000-000000000014'), | |
| 139 | +('f0030009-0001-4000-8000-000000000009', 'f0020002-0001-4000-8000-000000000002', 'f0010015-0001-4000-8000-000000000015'), | |
| 140 | +('f0030010-0001-4000-8000-000000000010', 'f0020002-0001-4000-8000-000000000002', 'f0010020-0001-4000-8000-000000000020'), | |
| 141 | +('f0030011-0001-4000-8000-000000000011', 'f0020002-0001-4000-8000-000000000002', 'f0010021-0001-4000-8000-000000000021'), | |
| 142 | +('f0030012-0001-4000-8000-000000000012', 'f0020002-0001-4000-8000-000000000002', 'f0010022-0001-4000-8000-000000000022'), | |
| 143 | +('f0030013-0001-4000-8000-000000000013', 'f0020002-0001-4000-8000-000000000002', 'f0010023-0001-4000-8000-000000000023'), | |
| 144 | +('f0030014-0001-4000-8000-000000000014', 'f0020002-0001-4000-8000-000000000002', 'f0010024-0001-4000-8000-000000000024'), | |
| 145 | +('f0030015-0001-4000-8000-000000000015', 'f0020002-0001-4000-8000-000000000002', 'f0010025-0001-4000-8000-000000000025'), | |
| 146 | +('f0030016-0001-4000-8000-000000000016', 'f0020002-0001-4000-8000-000000000002', 'f0010026-0001-4000-8000-000000000026'), | |
| 147 | +('f0030017-0001-4000-8000-000000000017', 'f0020002-0001-4000-8000-000000000002', 'f0010030-0001-4000-8000-000000000030'), | |
| 148 | +('f0030018-0001-4000-8000-000000000018', 'f0020002-0001-4000-8000-000000000002', 'f0010031-0001-4000-8000-000000000031'), | |
| 149 | +('f0030019-0001-4000-8000-000000000019', 'f0020002-0001-4000-8000-000000000002', 'f0010032-0001-4000-8000-000000000032'), | |
| 150 | +('f0030020-0001-4000-8000-000000000020', 'f0020002-0001-4000-8000-000000000002', 'f0010033-0001-4000-8000-000000000033'), | |
| 151 | +('f0030021-0001-4000-8000-000000000021', 'f0020002-0001-4000-8000-000000000002', 'f0010034-0001-4000-8000-000000000034'), | |
| 152 | +('f0030022-0001-4000-8000-000000000022', 'f0020002-0001-4000-8000-000000000002', 'f0010035-0001-4000-8000-000000000035'), | |
| 153 | +('f0030023-0001-4000-8000-000000000023', 'f0020002-0001-4000-8000-000000000002', 'f0010036-0001-4000-8000-000000000036'), | |
| 154 | +('f0030024-0001-4000-8000-000000000024', 'f0020002-0001-4000-8000-000000000002', 'f0010037-0001-4000-8000-000000000037'), | |
| 155 | +('f0030025-0001-4000-8000-000000000025', 'f0020002-0001-4000-8000-000000000002', 'f0010038-0001-4000-8000-000000000038'), | |
| 156 | +('f0030026-0001-4000-8000-000000000026', 'f0020002-0001-4000-8000-000000000002', 'f0010039-0001-4000-8000-000000000039') | |
| 157 | +ON DUPLICATE KEY UPDATE | |
| 158 | + `RoleId` = VALUES(`RoleId`), | |
| 159 | + `MenuId` = VALUES(`MenuId`); | ... | ... |
项目相关文档/th-tenant-menu-seed.sql
0 → 100644
| 1 | +-- 泰额版 menu 菜单初始化脚本(平台主库 / 租户业务库通用) | |
| 2 | +-- 执行库示例:antis-foodlabeling-host(平台主库);租户业务库按需切换 | |
| 3 | +-- 禁止对 antis-foodlabeling-us 执行(美国版) | |
| 4 | +-- | |
| 5 | +-- MenuType: 0=目录, 1=菜单, 2=组件/按钮 | |
| 6 | +-- MenuSource: 0=Ruoyi, 1=Pure, 2=Vben5;当前固定 Ruoyi(0) | |
| 7 | +-- 根菜单 ParentId 用 '0';子菜单 ParentId 用父菜单 Guid | |
| 8 | +-- | |
| 9 | +-- ========== 固定 Guid 对照表(名称 → Id) ========== | |
| 10 | +-- 首页概览 f0010001-0001-4000-8000-000000000001 | |
| 11 | +-- 平台管理 f0010002-0001-4000-8000-000000000002 | |
| 12 | +-- SAAS 公司 f0010003-0001-4000-8000-000000000003 | |
| 13 | +-- 标签管理 f0010010-0001-4000-8000-000000000010 | |
| 14 | +-- 标签 f0010011-0001-4000-8000-000000000011 | |
| 15 | +-- 标签分类 f0010012-0001-4000-8000-000000000012 | |
| 16 | +-- 标签类型 f0010013-0001-4000-8000-000000000013 | |
| 17 | +-- 标签模板 f0010014-0001-4000-8000-000000000014 | |
| 18 | +-- 多选选项集 f0010015-0001-4000-8000-000000000015 | |
| 19 | +-- 业务模块 f0010020-0001-4000-8000-000000000020 | |
| 20 | +-- 培训 f0010021-0001-4000-8000-000000000021 | |
| 21 | +-- 告警 f0010022-0001-4000-8000-000000000022 | |
| 22 | +-- 任务 f0010023-0001-4000-8000-000000000023 | |
| 23 | +-- 传感器 f0010024-0001-4000-8000-000000000024 | |
| 24 | +-- 食物浪费 f0010025-0001-4000-8000-000000000025 | |
| 25 | +-- 电子标签 f0010026-0001-4000-8000-000000000026 | |
| 26 | +-- 管理 f0010030-0001-4000-8000-000000000030 | |
| 27 | +-- 账户管理 f0010031-0001-4000-8000-000000000031 | |
| 28 | +-- 系统菜单 f0010032-0001-4000-8000-000000000032 | |
| 29 | +-- 菜单管理 f0010033-0001-4000-8000-000000000033 | |
| 30 | +-- 设备 f0010034-0001-4000-8000-000000000034 | |
| 31 | +-- 报表 f0010035-0001-4000-8000-000000000035 | |
| 32 | +-- 发票 f0010036-0001-4000-8000-000000000036 | |
| 33 | +-- 二维码 f0010037-0001-4000-8000-000000000037 | |
| 34 | +-- 支持 f0010038-0001-4000-8000-000000000038 | |
| 35 | +-- API f0010039-0001-4000-8000-000000000039 | |
| 36 | +-- ================================================== | |
| 37 | + | |
| 38 | +-- 1a) 物理删除非法 Id(th-menu-*),避免留下脏数据行 | |
| 39 | +DELETE FROM `rolemenu` WHERE `MenuId` LIKE 'th-menu-%'; | |
| 40 | +DELETE FROM `menu` WHERE `Id` LIKE 'th-menu-%'; | |
| 41 | + | |
| 42 | +-- 1b) 同 PermissionCode 的其它旧行(非本脚本固定 Guid)做逻辑删除 | |
| 43 | +UPDATE `menu` | |
| 44 | +SET `IsDeleted` = 1, | |
| 45 | + `LastModificationTime` = NOW() | |
| 46 | +WHERE `IsDeleted` = 0 | |
| 47 | + AND ( | |
| 48 | + ( | |
| 49 | + `PermissionCode` IN ( | |
| 50 | + 'menu.dashboard.analytics', | |
| 51 | + 'menu.platform', | |
| 52 | + 'menu.platform.tenants', | |
| 53 | + 'menu.labeling', | |
| 54 | + 'menu.labeling.labels', | |
| 55 | + 'menu.labeling.categories', | |
| 56 | + 'menu.labeling.types', | |
| 57 | + 'menu.labeling.templates', | |
| 58 | + 'menu.labeling.multiple-options', | |
| 59 | + 'menu.modules', | |
| 60 | + 'menu.modules.training', | |
| 61 | + 'menu.modules.alerts', | |
| 62 | + 'menu.modules.tasks', | |
| 63 | + 'menu.modules.sensors', | |
| 64 | + 'menu.modules.food-waste', | |
| 65 | + 'menu.modules.e-label', | |
| 66 | + 'menu.management', | |
| 67 | + 'menu.management.account', | |
| 68 | + 'menu.management.system-menu', | |
| 69 | + 'menu.management.menu', | |
| 70 | + 'menu.management.devices', | |
| 71 | + 'menu.management.reports', | |
| 72 | + 'menu.management.invoices', | |
| 73 | + 'menu.management.qr-codes', | |
| 74 | + 'menu.management.support', | |
| 75 | + 'menu.management.api' | |
| 76 | + ) | |
| 77 | + AND `Id` NOT IN ( | |
| 78 | + 'f0010001-0001-4000-8000-000000000001', | |
| 79 | + 'f0010002-0001-4000-8000-000000000002', | |
| 80 | + 'f0010003-0001-4000-8000-000000000003', | |
| 81 | + 'f0010010-0001-4000-8000-000000000010', | |
| 82 | + 'f0010011-0001-4000-8000-000000000011', | |
| 83 | + 'f0010012-0001-4000-8000-000000000012', | |
| 84 | + 'f0010013-0001-4000-8000-000000000013', | |
| 85 | + 'f0010014-0001-4000-8000-000000000014', | |
| 86 | + 'f0010015-0001-4000-8000-000000000015', | |
| 87 | + 'f0010020-0001-4000-8000-000000000020', | |
| 88 | + 'f0010021-0001-4000-8000-000000000021', | |
| 89 | + 'f0010022-0001-4000-8000-000000000022', | |
| 90 | + 'f0010023-0001-4000-8000-000000000023', | |
| 91 | + 'f0010024-0001-4000-8000-000000000024', | |
| 92 | + 'f0010025-0001-4000-8000-000000000025', | |
| 93 | + 'f0010026-0001-4000-8000-000000000026', | |
| 94 | + 'f0010030-0001-4000-8000-000000000030', | |
| 95 | + 'f0010031-0001-4000-8000-000000000031', | |
| 96 | + 'f0010032-0001-4000-8000-000000000032', | |
| 97 | + 'f0010033-0001-4000-8000-000000000033', | |
| 98 | + 'f0010034-0001-4000-8000-000000000034', | |
| 99 | + 'f0010035-0001-4000-8000-000000000035', | |
| 100 | + 'f0010036-0001-4000-8000-000000000036', | |
| 101 | + 'f0010037-0001-4000-8000-000000000037', | |
| 102 | + 'f0010038-0001-4000-8000-000000000038', | |
| 103 | + 'f0010039-0001-4000-8000-000000000039' | |
| 104 | + ) | |
| 105 | + ) | |
| 106 | + ); | |
| 107 | + | |
| 108 | +-- 2) 插入 / 更新 26 条食品 SaaS 菜单(合法 Guid) | |
| 109 | +INSERT INTO `menu` | |
| 110 | +( | |
| 111 | + `Id`, | |
| 112 | + `IsDeleted`, | |
| 113 | + `CreationTime`, | |
| 114 | + `CreatorId`, | |
| 115 | + `LastModifierId`, | |
| 116 | + `LastModificationTime`, | |
| 117 | + `OrderNum`, | |
| 118 | + `State`, | |
| 119 | + `MenuName`, | |
| 120 | + `RouterName`, | |
| 121 | + `MenuType`, | |
| 122 | + `PermissionCode`, | |
| 123 | + `ParentId`, | |
| 124 | + `MenuIcon`, | |
| 125 | + `Router`, | |
| 126 | + `IsLink`, | |
| 127 | + `IsCache`, | |
| 128 | + `IsShow`, | |
| 129 | + `Remark`, | |
| 130 | + `Component`, | |
| 131 | + `MenuSource`, | |
| 132 | + `Query`, | |
| 133 | + `ConcurrencyStamp` | |
| 134 | +) | |
| 135 | +VALUES | |
| 136 | +('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()), | |
| 137 | + | |
| 138 | +('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()), | |
| 139 | +('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()), | |
| 140 | + | |
| 141 | +('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()), | |
| 142 | +('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()), | |
| 143 | +('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()), | |
| 144 | +('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()), | |
| 145 | +('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()), | |
| 146 | +('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()), | |
| 147 | + | |
| 148 | +('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()), | |
| 149 | +('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()), | |
| 150 | +('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()), | |
| 151 | +('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()), | |
| 152 | +('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()), | |
| 153 | +('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()), | |
| 154 | +('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()), | |
| 155 | + | |
| 156 | +('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()), | |
| 157 | +('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()), | |
| 158 | +('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()), | |
| 159 | +('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()), | |
| 160 | +('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()), | |
| 161 | +('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()), | |
| 162 | +('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()), | |
| 163 | +('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()), | |
| 164 | +('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()), | |
| 165 | +('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()) | |
| 166 | +ON DUPLICATE KEY UPDATE | |
| 167 | + `IsDeleted` = VALUES(`IsDeleted`), | |
| 168 | + `LastModificationTime` = NOW(), | |
| 169 | + `OrderNum` = VALUES(`OrderNum`), | |
| 170 | + `State` = VALUES(`State`), | |
| 171 | + `MenuName` = VALUES(`MenuName`), | |
| 172 | + `RouterName` = VALUES(`RouterName`), | |
| 173 | + `MenuType` = VALUES(`MenuType`), | |
| 174 | + `PermissionCode` = VALUES(`PermissionCode`), | |
| 175 | + `ParentId` = VALUES(`ParentId`), | |
| 176 | + `MenuIcon` = VALUES(`MenuIcon`), | |
| 177 | + `Router` = VALUES(`Router`), | |
| 178 | + `IsLink` = VALUES(`IsLink`), | |
| 179 | + `IsCache` = VALUES(`IsCache`), | |
| 180 | + `IsShow` = VALUES(`IsShow`), | |
| 181 | + `Remark` = VALUES(`Remark`), | |
| 182 | + `Component` = VALUES(`Component`), | |
| 183 | + `MenuSource` = VALUES(`MenuSource`), | |
| 184 | + `Query` = VALUES(`Query`), | |
| 185 | + `ConcurrencyStamp` = UUID(); | |
| 186 | + | |
| 187 | +-- 导入后可执行下面语句检查: | |
| 188 | +-- SELECT Id, ParentId, MenuName, RouterName, Router, MenuType, MenuSource, OrderNum, IsShow, State | |
| 189 | +-- FROM `menu` | |
| 190 | +-- WHERE IsDeleted = 0 | |
| 191 | +-- ORDER BY OrderNum ASC, MenuName ASC; | ... | ... |
项目相关文档/批量导入导出接口说明.md
| ... | ... | @@ -10,6 +10,7 @@ |
| 10 | 10 | |------|------| |
| 11 | 11 | | [公共约定](#公共约定) | 基址、鉴权、Swagger、通用注意事项 | |
| 12 | 12 | | [共享配置](#共享配置) | `appsettings` 中 `FoodLabeling:BatchImport` | |
| 13 | +| [在线批量导入(JSON,推荐)](#在线批量导入json推荐) | Region / Location / Team Member / Product 四模块 JSON 逐行导入 | | |
| 13 | 14 | | [1 Location Manager(门店)](#1-location-manager门店) | 下载模板 / Excel 导出 / 导入 / 批量编辑 | |
| 14 | 15 | | [2 Team Member(成员)](#2-team-member成员) | 下载模板 / **PDF 全量导出** / Excel 导入 / 批量编辑 | |
| 15 | 16 | | [3 Products(菜单-产品)](#3-products菜单-产品) | 下载模板 / Excel 全量导出 / Excel 导入 / 批量编辑 | |
| ... | ... | @@ -27,7 +28,7 @@ |
| 27 | 28 | - **Swagger 分组**:**「食品标签-美国版接口」**;具体路径以 Swagger 展示为准(下表为常见命名,联调时请以 Swagger 为准)。 |
| 28 | 29 | - **鉴权**:与其它业务接口相同,请求头携带登录接口返回的 **`data.token` 完整值**(已含 `Bearer ` 前缀),示例:`Authorization: {data.token}`。 |
| 29 | 30 | - **文件类响应**:`Content-Type` 多为 `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`(`.xlsx`)。 |
| 30 | -- **导入类请求**:统一使用 **`multipart/form-data`**,文件字段名以各接口说明为准(Location 为 **`file`**)。 | |
| 31 | +- **导入类请求**:Excel 导入统一使用 **`multipart/form-data`**,文件字段名以各接口说明为准(Location 为 **`file`**);**推荐**使用下文 [在线批量导入(JSON)](#在线批量导入json推荐) 替代 Excel 上传。 | |
| 31 | 32 | - **批量编辑类请求**:使用 **`application/json`**,一次提交多行(与前端表格「保存全部」对齐)。 |
| 32 | 33 | - **导出类响应**:Location 与 **Products(菜单-产品)**、**Reports — Print Log** 为 **Excel 全量**;**Team Member**、**Account Management 的 Company / Region**、**Reports — Label Report** 等为 **PDF**(见各小节);数据量极大时请注意服务端内存与响应耗时。 |
| 33 | 34 | |
| ... | ... | @@ -52,6 +53,81 @@ |
| 52 | 53 | |
| 53 | 54 | --- |
| 54 | 55 | |
| 56 | +## 在线批量导入(JSON,推荐) | |
| 57 | + | |
| 58 | +四个 Account Management / 业务模块均提供 **JSON 在线批量导入** 接口:请求体 `{ "items": [ ...CreateInputVo ] }`,**逐行调用现有 `CreateAsync`**,**部分成功**(单行失败不影响其它行)。`errors` 中 **`index` 从 0 起**(与 `items` 数组下标一致)。单次条数上限为 **`MaxImportRows`**(默认 5000)。 | |
| 59 | + | |
| 60 | +| 模块 | 方法 | HTTP | 路径 | 失败行 key 字段 | | |
| 61 | +|------|------|------|------|-----------------| | |
| 62 | +| Region(Group) | `BatchImportOnlineAsync` | `POST` | `/api/app/group/batch-import-online` | `groupName` | | |
| 63 | +| Location | `BatchImportOnlineAsync` | `POST` | `/api/app/location/batch-import-online` | `locationCode` | | |
| 64 | +| Team Member | `BatchImportOnlineAsync` | `POST` | `/api/app/team-member/batch-import-online` | `userName` | | |
| 65 | +| Product | `BatchImportOnlineAsync` | `POST` | `/api/app/product/batch-import-online` | `productName` | | |
| 66 | + | |
| 67 | +**公共返回字段**(各模块 `*BatchImportOnlineResultDto`):`successCount`、`failCount`、`errors[{ index, message, key? }]`(JSON 命名一般为 camelCase)。 | |
| 68 | + | |
| 69 | +**Team Member 特殊规则**:`password` 为空或仅空白时,使用配置 **`TeamMemberImportDefaultPassword`**;若配置亦为空则该行失败。 | |
| 70 | + | |
| 71 | +**与 Excel 导入关系**:Excel 导入接口(`import-*-batch`)**保留但不推荐**,新前端与联调请优先 **`batch-import-online`**;Excel 仍适用于离线模板填写后上传的旧流程。 | |
| 72 | + | |
| 73 | +### Region(Group)在线导入 | |
| 74 | + | |
| 75 | +| 项目 | 说明 | | |
| 76 | +|------|------| | |
| 77 | +| Body | `GroupBatchImportOnlineInputVo`:`items` 为 `GroupCreateInputVo` 数组(`groupName`、`partnerId`、`state`) | | |
| 78 | +| 业务 | 与单条 `POST /api/app/group` 一致;ID 由现有 `CreateAsync` 生成 | | |
| 79 | + | |
| 80 | +```bash | |
| 81 | +curl -X POST "$BASE/api/app/group/batch-import-online" \ | |
| 82 | + -H "Authorization: TOKEN" \ | |
| 83 | + -H "Content-Type: application/json" \ | |
| 84 | + -d "{\"items\":[{\"groupName\":\"NC Region\",\"partnerId\":\"PARTNER_ID\",\"state\":true}]}" | |
| 85 | +``` | |
| 86 | + | |
| 87 | +### Location 在线导入 | |
| 88 | + | |
| 89 | +| 项目 | 说明 | | |
| 90 | +|------|------| | |
| 91 | +| Body | `LocationBatchImportOnlineInputVo`:`items` 为 `LocationCreateInputVo` 数组 | | |
| 92 | +| 业务 | 与单条 `POST /api/app/location` 一致 | | |
| 93 | + | |
| 94 | +```bash | |
| 95 | +curl -X POST "$BASE/api/app/location/batch-import-online" \ | |
| 96 | + -H "Authorization: TOKEN" \ | |
| 97 | + -H "Content-Type: application/json" \ | |
| 98 | + -d "{\"items\":[{\"locationCode\":\"LOC001\",\"locationName\":\"UNCC store\",\"partner\":\"MedVantage Cafe Group\",\"groupName\":\"NC Region\",\"state\":true}]}" | |
| 99 | +``` | |
| 100 | + | |
| 101 | +### Team Member 在线导入 | |
| 102 | + | |
| 103 | +| 项目 | 说明 | | |
| 104 | +|------|------| | |
| 105 | +| Body | `TeamMemberBatchImportOnlineInputVo`:`items` 为 `TeamMemberCreateInputVo` 数组 | | |
| 106 | +| 密码 | 未填 `password` 时使用 `TeamMemberImportDefaultPassword` | | |
| 107 | + | |
| 108 | +```bash | |
| 109 | +curl -X POST "$BASE/api/app/team-member/batch-import-online" \ | |
| 110 | + -H "Authorization: TOKEN" \ | |
| 111 | + -H "Content-Type: application/json" \ | |
| 112 | + -d "{\"items\":[{\"fullName\":\"John Doe\",\"userName\":\"john@example.com\",\"email\":\"john@example.com\",\"roleId\":\"ROLE_GUID\",\"locationIds\":[\"LOCATION_GUID\"],\"state\":true}]}" | |
| 113 | +``` | |
| 114 | + | |
| 115 | +### Product 在线导入 | |
| 116 | + | |
| 117 | +| 项目 | 说明 | | |
| 118 | +|------|------| | |
| 119 | +| Body | `ProductBatchImportOnlineInputVo`:`items` 为 `ProductCreateInputVo` 数组 | | |
| 120 | +| 业务 | 与单条 `POST /api/app/product` 一致 | | |
| 121 | + | |
| 122 | +```bash | |
| 123 | +curl -X POST "$BASE/api/app/product/batch-import-online" \ | |
| 124 | + -H "Authorization: TOKEN" \ | |
| 125 | + -H "Content-Type: application/json" \ | |
| 126 | + -d "{\"items\":[{\"productName\":\"Tuna & Bacon Sub\",\"categoryId\":\"CATEGORY_ID\",\"productCode\":\"40001\",\"state\":true,\"locationIds\":[\"LOCATION_GUID\"]}]}" | |
| 127 | +``` | |
| 128 | + | |
| 129 | +--- | |
| 130 | + | |
| 55 | 131 | ## 1 Location Manager(门店) |
| 56 | 132 | |
| 57 | 133 | **应用服务**:`LocationAppService`(模块 `food-labeling-us`)。 |
| ... | ... | @@ -80,7 +156,7 @@ |
| 80 | 156 | | 排序 | 与列表一致:有 `Sorting` 则按其排序,否则默认 `CreationTime` 降序 | |
| 81 | 157 | | 响应文件名示例 | `locations-export-yyyyMMdd-HHmmss.xlsx` | |
| 82 | 158 | |
| 83 | -### 1.3 批量导入 Excel | |
| 159 | +### 1.3 批量导入 Excel(不推荐,请优先 [batch-import-online](#在线批量导入json推荐)) | |
| 84 | 160 | |
| 85 | 161 | | 项目 | 说明 | |
| 86 | 162 | |------|------| |
| ... | ... | @@ -182,7 +258,7 @@ |
| 182 | 258 | |
| 183 | 259 | **说明**:PDF 中「Assigned Locations」展示该成员**全部**已分配门店(不受列表按门店筛选时「仅显示命中门店」的收缩影响),便于导出后审阅完整权限。 |
| 184 | 260 | |
| 185 | -### 2.3 批量导入 Excel | |
| 261 | +### 2.3 批量导入 Excel(不推荐,请优先 [batch-import-online](#在线批量导入json推荐)) | |
| 186 | 262 | |
| 187 | 263 | | 项目 | 说明 | |
| 188 | 264 | |------|------| |
| ... | ... | @@ -255,7 +331,7 @@ |
| 255 | 331 | |
| 256 | 332 | **部署**:须使用 **`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` 结束时报错提示。 |
| 257 | 333 | |
| 258 | -### 3.3 批量导入 Excel | |
| 334 | +### 3.3 批量导入 Excel(不推荐,请优先 [batch-import-online](#在线批量导入json推荐)) | |
| 259 | 335 | |
| 260 | 336 | | 项目 | 说明 | |
| 261 | 337 | |------|------| |
| ... | ... | @@ -429,6 +505,27 @@ curl -X PUT "$BASE/api/app/product/products-bulk" \ |
| 429 | 505 | -H "Authorization: TOKEN" \ |
| 430 | 506 | -H "Content-Type: application/json" \ |
| 431 | 507 | -d "{\"items\":[{\"id\":\"YOUR_PRODUCT_ID\",\"productName\":\"Tuna & Bacon Sub\",\"categoryId\":\"CATEGORY_ID\",\"productCode\":\"40001\",\"state\":true,\"locationIds\":[\"LOCATION_GUID_1\",\"LOCATION_GUID_2\"]}]}" |
| 508 | + | |
| 509 | +# --- 在线批量导入(JSON,推荐)--- | |
| 510 | +curl -X POST "$BASE/api/app/group/batch-import-online" \ | |
| 511 | + -H "Authorization: TOKEN" \ | |
| 512 | + -H "Content-Type: application/json" \ | |
| 513 | + -d "{\"items\":[{\"groupName\":\"NC Region\",\"partnerId\":\"PARTNER_ID\",\"state\":true}]}" | |
| 514 | + | |
| 515 | +curl -X POST "$BASE/api/app/location/batch-import-online" \ | |
| 516 | + -H "Authorization: TOKEN" \ | |
| 517 | + -H "Content-Type: application/json" \ | |
| 518 | + -d "{\"items\":[{\"locationCode\":\"LOC001\",\"locationName\":\"UNCC store\",\"state\":true}]}" | |
| 519 | + | |
| 520 | +curl -X POST "$BASE/api/app/team-member/batch-import-online" \ | |
| 521 | + -H "Authorization: TOKEN" \ | |
| 522 | + -H "Content-Type: application/json" \ | |
| 523 | + -d "{\"items\":[{\"fullName\":\"John\",\"userName\":\"john@example.com\",\"email\":\"john@example.com\",\"roleId\":\"ROLE_GUID\",\"state\":true}]}" | |
| 524 | + | |
| 525 | +curl -X POST "$BASE/api/app/product/batch-import-online" \ | |
| 526 | + -H "Authorization: TOKEN" \ | |
| 527 | + -H "Content-Type: application/json" \ | |
| 528 | + -d "{\"items\":[{\"productName\":\"Tuna Sub\",\"categoryId\":\"CATEGORY_ID\",\"state\":true}]}" | |
| 432 | 529 | ``` |
| 433 | 530 | |
| 434 | 531 | 更完整的接口测试流程见仓库内 `.codex/skills/api-interface-testing/SKILL.md`。 | ... | ... |