From aa01fd36272d7b1baa032b7cb695af1876b63cf7 Mon Sep 17 00:00:00 2001 From: 李曜臣 Date: Fri, 7 Aug 2026 17:08:30 +0800 Subject: [PATCH] 2026-08-07代码优化 --- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/SqlSugarDbContextFactory.cs | 38 ++++++++++++++++++++++++++++++++------ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs | 13 +++++++++++++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileDto.cs | 21 +++++++++++++++++++++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeInputVo.cs | 5 ++++- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUpdateInputVo.cs | 31 ++++++++++++++++++++++++++++++- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadInputVo.cs | 40 +++++++++++++++++++++++++++++++++++++++- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITrainingAppService.cs | 5 +++-- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs | 3 +++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj | 3 +++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs | 6 ++++-- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs | 12 ++++++++++++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs | 25 +++++++++++++++++++++++++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs | 18 ++++++++++++++++++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs | 13 +++++++++++-- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/FoodLabelingTenantMigrationScriptNames.cs | 5 ++++- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs | 182 ++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TrainingAppService.cs | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs | 9 ++------- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs | 18 ++++++++++++++++++ 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/Auth/ThAppLoginInputVo.cs | 9 +++++---- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/IServices/IThAppAuthAppService.cs | 3 ++- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Filters/FoodLabelingBusinessTenantActionFilter.cs | 1 + 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThTenantHostDataAccessor.cs | 34 ++++++++++++++++++++++++++++++---- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThAppAuthAppService.cs | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs | 39 ++++----------------------------------- 泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json | 2 +- 项目相关文档/培训接口文档.md | 391 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 27 files changed, 877 insertions(+), 453 deletions(-) diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/SqlSugarDbContextFactory.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/SqlSugarDbContextFactory.cs index 689fff6..dd28a19 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/SqlSugarDbContextFactory.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/SqlSugarDbContextFactory.cs @@ -82,25 +82,51 @@ namespace Yi.Framework.SqlSugarCore private ISqlSugarClient GetOrCreateClient() { - if (_sqlSugarClient is not null) + // 必须每次按当前租户解析连接串:MultiTenancyMiddleware 查 YiTenant 时会先 Change(null) + // 创建主库客户端;若缓存不切换,后续业务会误连 host。 + var tenantConfiguration = + AsyncHelper.RunSync(async () => await TenantConfigurationWrapper.GetAsync()); + var connectionString = tenantConfiguration.GetCurrentConnectionString(); + var dbType = GetCurrentDbType(tenantConfiguration.GetCurrentConnectionName()); + + if (_sqlSugarClient is not null && + string.Equals( + _sqlSugarClient.CurrentConnectionConfig?.ConnectionString, + connectionString, + StringComparison.Ordinal)) { return _sqlSugarClient; } lock (_clientLock) { - if (_sqlSugarClient is not null) + if (_sqlSugarClient is not null && + string.Equals( + _sqlSugarClient.CurrentConnectionConfig?.ConnectionString, + connectionString, + StringComparison.Ordinal)) { return _sqlSugarClient; } - var tenantConfiguration = - AsyncHelper.RunSync(async () => await TenantConfigurationWrapper.GetAsync()); + if (_sqlSugarClient is not null) + { + try + { + _sqlSugarClient.Dispose(); + } + catch + { + // ignore dispose race + } + + _sqlSugarClient = null; + } var connectionConfig = BuildConnectionConfig(options => { - options.ConnectionString = tenantConfiguration.GetCurrentConnectionString(); - options.DbType = GetCurrentDbType(tenantConfiguration.GetCurrentConnectionName()); + options.ConnectionString = connectionString; + options.DbType = dbType; }); var client = new SqlSugarClient(connectionConfig); diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs index 4badb3f..aa46c7d 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs @@ -37,6 +37,11 @@ public class ProductCreateInputVo public string? AvailabilityType { get; set; } /// + /// 适用 Company:ALL / SPECIFIED(产品 Company 仅支持 SPECIFIED + 单选具体 Guid)。 + /// + public string? AppliedPartnerType { get; set; } + + /// /// 适用 Company(fl_partner.Id,UI 称 Company);展开该公司下全部门店后与 Region/门店合并写入 fl_location_product。 /// 与 二选一或同值;产品 Company 仅支持单选不支持 ALL。 /// 兼容前端误传数组:"partnerId":["{guid}"] 与字符串等价。 @@ -45,11 +50,19 @@ public class ProductCreateInputVo public string? PartnerId { get; set; } /// + /// 适用 Company Id 列表(与 同义,前端常用字段)。仅支持单选。 + /// + public List? PartnerIds { get; set; } + + /// /// 适用 Company(推荐前端字段)。仅支持单选:数组最多 1 个具体 Guid;不支持 ALL。 /// 传多个 Guid 或含 ALL 将报错;与 同时传时须一致。 /// public List? CompanyIds { get; set; } + /// 适用 Region:ALL / SPECIFIED(与 独立,用于前端显式声明) + public string? AppliedRegionType { get; set; } + /// /// 适用 Region(fl_group.Id,UI 称 Region;库字段为 location.GroupName)。 /// 与 合并;含 ALL 且无具体门店时,有具体 Company 则展开该公司门店,否则归档全局 ALL。 diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileDto.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileDto.cs index 23ed30a..1f0306c 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileDto.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileDto.cs @@ -19,10 +19,31 @@ public class TrainingFileDto public string AppliedPartnerType { get; set; } = "ALL"; + /// Company 展示(ALL 时为 All Companies) + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + /// 相同 + public List CompanyIds { get; set; } = new(); + public string AppliedRegionType { get; set; } = "ALL"; + /// Region 展示 + public string Region { get; set; } = string.Empty; + + public List RegionIds { get; set; } = new(); + + /// 相同 + public List GroupIds { get; set; } = new(); + public string AvailabilityType { get; set; } = "ALL"; + /// Location 展示 + public string Location { get; set; } = string.Empty; + + public List LocationIds { get; set; } = new(); + public DateTime CreationTime { get; set; } public DateTime? LastModificationTime { get; set; } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeInputVo.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeInputVo.cs index e41ddfd..bfc1e58 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeInputVo.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeInputVo.cs @@ -2,7 +2,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; namespace FoodLabeling.Application.Contracts.Dtos.Training; -public class TrainingFileScopeInputVo : ILabelEntityPartnerScopeInput +public class TrainingFileScopeInputVo : ITrainingFileScopeInput { public string? AppliedPartnerType { get; set; } @@ -20,5 +20,8 @@ public class TrainingFileScopeInputVo : ILabelEntityPartnerScopeInput /// 适用 Location:ALL / SPECIFIED public string? AvailabilityType { get; set; } + /// 适用 Location:ALL / SPECIFIED( 别名) + public string? AppliedLocationType { get; set; } + public List? LocationIds { get; set; } } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUpdateInputVo.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUpdateInputVo.cs index 375ac79..9cc01c7 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUpdateInputVo.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUpdateInputVo.cs @@ -1,8 +1,37 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; + namespace FoodLabeling.Application.Contracts.Dtos.Training; -public class TrainingFileUpdateInputVo +public class TrainingFileUpdateInputVo : ITrainingFileScopeInput { public string FileName { get; set; } = string.Empty; public int OrderNum { get; set; } + + /// 适用 Company:ALL / SPECIFIED + public string? AppliedPartnerType { get; set; } + + /// 适用 Company(fl_partner.Id);可含 ALL + public List? PartnerIds { get; set; } + + /// 相同;可含 ALL + public List? CompanyIds { get; set; } + + /// 适用 Region:ALL / SPECIFIED + public string? AppliedRegionType { get; set; } + + /// 适用 Region(fl_group.Id);与 合并;可含 ALL + public List? RegionIds { get; set; } + + /// 相同;可含 ALL + public List? GroupIds { get; set; } + + /// 适用 Location:ALL / SPECIFIED + public string? AvailabilityType { get; set; } + + /// 适用 Location:ALL / SPECIFIED( 别名) + public string? AppliedLocationType { get; set; } + + /// 适用门店(location.Id);可含 ALL + public List? LocationIds { get; set; } } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadInputVo.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadInputVo.cs index 93452ed..f92b59a 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadInputVo.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadInputVo.cs @@ -1,9 +1,10 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace FoodLabeling.Application.Contracts.Dtos.Training; -public class TrainingFileUploadInputVo +public class TrainingFileUploadInputVo : ITrainingFileScopeInput { [FromForm(Name = "file")] public IFormFile File { get; set; } = default!; @@ -11,5 +12,42 @@ public class TrainingFileUploadInputVo [FromForm(Name = "categoryId")] public string CategoryId { get; set; } = string.Empty; + [FromForm(Name = "orderNum")] public int OrderNum { get; set; } + + /// 适用 Company:ALL / SPECIFIED + [FromForm(Name = "appliedPartnerType")] + public string? AppliedPartnerType { get; set; } + + /// 适用 Company(fl_partner.Id);可含 ALL + [FromForm(Name = "partnerIds")] + public List? PartnerIds { get; set; } + + /// 相同;可含 ALL + [FromForm(Name = "companyIds")] + public List? CompanyIds { get; set; } + + /// 适用 Region:ALL / SPECIFIED + [FromForm(Name = "appliedRegionType")] + public string? AppliedRegionType { get; set; } + + /// 适用 Region(fl_group.Id);与 合并;可含 ALL + [FromForm(Name = "regionIds")] + public List? RegionIds { get; set; } + + /// 相同;可含 ALL + [FromForm(Name = "groupIds")] + public List? GroupIds { get; set; } + + /// 适用 Location:ALL / SPECIFIED + [FromForm(Name = "availabilityType")] + public string? AvailabilityType { get; set; } + + /// 适用 Location:ALL / SPECIFIED( 别名) + [FromForm(Name = "appliedLocationType")] + public string? AppliedLocationType { get; set; } + + /// 适用门店(location.Id);可含 ALL + [FromForm(Name = "locationIds")] + public List? LocationIds { get; set; } } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITrainingAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITrainingAppService.cs index 70b9809..f9ec1f1 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITrainingAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITrainingAppService.cs @@ -1,8 +1,9 @@ using FoodLabeling.Application.Contracts.Dtos.Training; +using Volo.Abp.Application.Services; namespace FoodLabeling.Application.Contracts.IServices; -public interface ITrainingAppService +public interface ITrainingAppService : IApplicationService { Task> GetCategoryTreeAsync(TrainingCategoryTreeInputVo input); @@ -12,7 +13,7 @@ public interface ITrainingAppService Task DeleteCategoryAsync(string id); - Task UploadFileAsync(TrainingFileUploadInputVo input); + Task UploadFileAsync(TrainingFileUploadInputVo input); Task UpdateFileAsync(string id, TrainingFileUpdateInputVo input); diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs index 2727138..faa7318 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs @@ -10,4 +10,7 @@ public static class UsAppJwtClaims /// 美国版移动端 App public const string ClientKindUsApp = "us-app"; + + /// 泰额版 App(us-app-auth 转发 th-app-auth 签发) + public const string ClientKindThApp = "th_app"; } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj index 435bd29..0b777f9 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj @@ -27,6 +27,9 @@ FoodLabeling.TenantMigrations.fl_training.sql + + FoodLabeling.TenantMigrations.fl_label_alert_timer.sql + diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs index aa9ffa8..63aebad 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs @@ -454,6 +454,8 @@ public static class AllScopeBindingHelper var locationHasAll = HasAllScopeSentinelSelection(normalizedLocationIds); var regionHasAllSentinel = HasAllScopeSentinelSelection(mergedRegionIds); var hasConcretePartner = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext).Count > 0; + // 用户显式传具体门店 Id 时一律 SPECIFIED,不因「恰好覆盖 Region 全集」归档 ALL + var userExplicitLocations = explicitLocationIds.Count > 0 && !locationHasAll; // 前端 Select All Region 常展开为全量 Guid,需识别为 Region=ALL var regionHasAll = regionHasAllSentinel; @@ -467,7 +469,7 @@ public static class AllScopeBindingHelper // location ALL 哨兵,或具体 Region 下「空选/全选门店」:有 Company/Region 上下文则展开 SPECIFIED var locationCoversConcreteRegions = false; - if (!locationHasAll && regionIds.Count > 0 && !regionHasAll) + if (!locationHasAll && !userExplicitLocations && regionIds.Count > 0 && !regionHasAll) { if (explicitLocationIds.Count == 0) { @@ -484,7 +486,7 @@ public static class AllScopeBindingHelper } } - if (locationHasAll || locationCoversConcreteRegions) + if (!userExplicitLocations && (locationHasAll || locationCoversConcreteRegions)) { var expandRegions = regionHasAll ? null : (regionIds.Count > 0 ? regionIds : null); var expanded = await LocationScopeBindingHelper.ExpandScopedAllLocationsForSaveAsync( diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs index d7a7fd1..0cbe0ca 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs @@ -67,6 +67,18 @@ public static class ReportsPrintLogExpiryHelper public static string ExtractFormattedExpiryText(string? printInputJson) => ExtractFormattedExpiryText(printInputJson, null, null, null); + /// 与 Print Log Expiration 同源:解析绝对过期时刻。 + public static bool TryResolveExpiryDateTime( + string? printInputJson, + string? renderTemplateJson, + DateTime? baseTime, + DateTime? printedAt, + out DateTime expiresAt) + { + var reference = baseTime ?? printedAt ?? DateTime.Now; + return TryResolveExpiryDateTime(printInputJson, renderTemplateJson, reference, out expiresAt); + } + private static bool TryResolveExpiryDateTime( string? printInputJson, string? renderTemplateJson, diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs index e856247..759880d 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs @@ -1,3 +1,4 @@ +using SqlSugar; using Volo.Abp.Users; using Yi.Framework.Rbac.Domain.Shared.Consts; @@ -66,4 +67,28 @@ public static class ReportsRoleHelper return false; } + + /// + /// App JWT 未写入角色声明时,按业务库 UserRole/Role 判断是否管理员。 + /// + public static async Task IsAdminRoleFromDbAsync(ISqlSugarClient db, Guid userId) + { + var rows = await db.Ado.SqlQueryAsync( + """ + SELECT r.RoleCode + FROM UserRole ur + INNER JOIN Role r ON ur.RoleId = r.Id + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 + """, + new { UserId = userId }); + + return rows.Any(r => + !string.IsNullOrWhiteSpace(r.RoleCode) && + string.Equals(r.RoleCode.Trim(), UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)); + } + + private sealed class AdminRoleCodeRow + { + public string? RoleCode { get; set; } + } } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs index 0dbca20..543fa71 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs @@ -187,6 +187,24 @@ public static class TrainingFileScopeHelper } /// + /// 构建多个培训文件的适用范围回显(列表/树批量用)。 + /// + public static async Task> BuildScopeDisplayMapAsync( + ISqlSugarClient db, + IReadOnlyList files) + { + if (files.Count == 0) + { + return new Dictionary(StringComparer.Ordinal); + } + + var tasks = files.Select(async file => + (file.Id, Display: await BuildScopeDisplayAsync(db, file))); + var results = await Task.WhenAll(tasks); + return results.ToDictionary(x => x.Id, x => x.Display, StringComparer.Ordinal); + } + + /// /// 删除培训文件全部范围关联。 /// public static async Task DeleteScopeRowsAsync(ISqlSugarClient db, string trainingFileId) diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs index f9f4c8c..d4ef904 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs @@ -27,7 +27,10 @@ public static class UsAppAuthScopeHelper } var kind = currentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value; - if (!string.Equals(kind, UsAppJwtClaims.ClientKindUsApp, StringComparison.Ordinal)) + // 美国版 us-app;泰额版 us-app-auth 转发 th-app-auth 后签发 th_app + var isAppToken = string.Equals(kind, UsAppJwtClaims.ClientKindUsApp, StringComparison.Ordinal) + || string.Equals(kind, UsAppJwtClaims.ClientKindThApp, StringComparison.Ordinal); + if (!isAppToken) { throw new UserFriendlyException("请使用 App 登录令牌调用该接口"); } @@ -53,7 +56,13 @@ public static class UsAppAuthScopeHelper return true; } - return await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser); + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)) + { + return true; + } + + // App JWT 可能未写 role claim:回退查库 RoleCode=admin + return await ReportsRoleHelper.IsAdminRoleFromDbAsync(db, currentUser.Id.Value); } public static async Task> ListCompaniesAsync(ISqlSugarClient db) diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/FoodLabelingTenantMigrationScriptNames.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/FoodLabelingTenantMigrationScriptNames.cs index a9cce58..50c1523 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/FoodLabelingTenantMigrationScriptNames.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/FoodLabelingTenantMigrationScriptNames.cs @@ -18,6 +18,8 @@ public static class FoodLabelingTenantMigrationScriptNames public const string Training = "FoodLabeling.TenantMigrations.fl_training.sql"; + public const string LabelAlertTimer = "FoodLabeling.TenantMigrations.fl_label_alert_timer.sql"; + /// CodeFirst 之后按序执行的脚本资源名。 public static readonly IReadOnlyList TenantInitializationOrder = [ @@ -26,6 +28,7 @@ public static class FoodLabelingTenantMigrationScriptNames ProductCategoryPartnerScope, UserLocation, TeamMemberScope, - Training + Training, + LabelAlertTimer ]; } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs index a671185..712b34e 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs @@ -193,11 +193,10 @@ public class ProductAppService : ApplicationService, IProductAppService partnerIds, groupIds, locationIds, - new ScopeAllEchoHelper.ScopeAllEchoOptions - { - IsRegionAll = AllScopeBindingHelper.IsDeclaredAll(entity.AppliedRegionType), - IsLocationAll = false - }); + ScopeAllEchoHelper.ForLabelEntityScope( + appliedPartnerType: null, + appliedRegionType: entity.AppliedRegionType, + availabilityType: entity.AvailabilityType)); } List companyIdsForEcho; @@ -972,6 +971,7 @@ public class ProductAppService : ApplicationService, IProductAppService private static bool HasProductScopeBinding(ProductCreateInputVo input) => !string.IsNullOrWhiteSpace(input.PartnerId) || + input.PartnerIds is not null || input.CompanyIds is not null || input.GroupIds is not null || input.RegionIds is not null || @@ -1004,7 +1004,18 @@ public class ProductAppService : ApplicationService, IProductAppService /// private static string? ResolveSinglePartnerIdForSave(ProductCreateInputVo input) { - var companyIds = LocationScopeBindingHelper.NormalizeIds(input.CompanyIds); + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.CompanyIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) + { + merged.Add(id); + } + + var companyIds = merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); if (AllScopeBindingHelper.HasAllScopeSentinelSelection(companyIds) || LocationScopeBindingHelper.IsAllScopeSentinel(input.PartnerId)) { @@ -1045,159 +1056,18 @@ public class ProductAppService : ApplicationService, IProductAppService } var partnerId = ResolveSinglePartnerIdForSave(input); - var normalizedLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); - var normalizedGroupIds = NormalizeProductRegionIds(input); - var locationHasAll = AllScopeBindingHelper.HasAllScopeSentinelSelection(normalizedLocationIds); - var regionHasAllSentinel = AllScopeBindingHelper.HasAllScopeSentinelSelection(normalizedGroupIds); - var groupIds = LocationScopeBindingHelper.FilterConcreteScopeIds(normalizedGroupIds); - var explicitLocationIds = LocationScopeBindingHelper.FilterConcreteScopeIds(normalizedLocationIds); - var hasConcretePartner = !string.IsNullOrWhiteSpace(partnerId); - var partnerContext = hasConcretePartner ? new[] { partnerId! } : null; - - var regionHasAll = regionHasAllSentinel; - if (!regionHasAll && groupIds.Count > 0) - { - var allRegions = await AllScopeBindingHelper.ResolveAllRegionIdsAsync( - _dbContext.SqlSugarClient, partnerContext); - regionHasAll = allRegions.Count > 0 - && AllScopeBindingHelper.IsFullIdSelection(groupIds, allRegions); - } - - // Location ALL 哨兵,或具体 Region 下空选/全选门店(前端常展开为 Guid) - var locationCoversConcreteRegions = false; - if (!locationHasAll && groupIds.Count > 0 && !regionHasAll) - { - if (explicitLocationIds.Count == 0) - { - locationCoversConcreteRegions = true; - } - else - { - var regionUniverse = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( - _dbContext.SqlSugarClient, partnerContext, groupIds); - locationCoversConcreteRegions = regionUniverse.Count > 0 - && AllScopeBindingHelper.IsFullIdSelection( - explicitLocationIds, regionUniverse); - } - } - - if (locationHasAll || locationCoversConcreteRegions) - { - var expandRegions = regionHasAll ? null : (groupIds.Count > 0 ? groupIds : null); - var expanded = await LocationScopeBindingHelper.ExpandScopedAllLocationsForSaveAsync( - _dbContext.SqlSugarClient, - partnerContext, - expandRegions); - if (expanded is not null) - { - return ( - AllScopeBindingHelper.ScopeSpecified, - regionHasAll || groupIds.Count == 0 - ? AllScopeBindingHelper.ScopeAll - : AllScopeBindingHelper.ScopeSpecified, - expanded); - } - - return (AllScopeBindingHelper.ScopeAll, AllScopeBindingHelper.ScopeAll, new List()); - } - - // Region=ALL + 具体门店:保留门店快照 - if (regionHasAll && explicitLocationIds.Count > 0) - { - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( - _dbContext.SqlSugarClient, explicitLocationIds); - return ( - AllScopeBindingHelper.ScopeSpecified, - AllScopeBindingHelper.ScopeAll, - explicitLocationIds); - } + var mergedRegionIds = NormalizeProductRegionIds(input); + var partnerContext = !string.IsNullOrWhiteSpace(partnerId) ? new[] { partnerId! } : null; - if (explicitLocationIds.Count > 0) - { - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( - _dbContext.SqlSugarClient, explicitLocationIds); - return ( - AllScopeBindingHelper.ScopeSpecified, - AllScopeBindingHelper.ScopeSpecified, - explicitLocationIds); - } - - if (regionHasAll) - { - var expanded = await LocationScopeBindingHelper.ExpandScopedAllLocationsForSaveAsync( - _dbContext.SqlSugarClient, - partnerContext, - regionIds: null); - if (expanded is not null) - { - return ( - AllScopeBindingHelper.ScopeSpecified, - AllScopeBindingHelper.ScopeAll, - expanded); - } - - return (AllScopeBindingHelper.ScopeAll, AllScopeBindingHelper.ScopeAll, new List()); - } - - if (await AllScopeBindingHelper.ShouldTreatProductLocationScopeAsAllAsync( - _dbContext.SqlSugarClient, - input.AvailabilityType, - partnerId, - groupIds, - explicitLocationIds, - HasProductScopeBinding(input))) - { - return (AllScopeBindingHelper.ScopeAll, AllScopeBindingHelper.ScopeAll, new List()); - } - - if (!HasProductScopeBinding(input)) - { - return (AllScopeBindingHelper.ScopeSpecified, AllScopeBindingHelper.ScopeAll, new List()); - } - - var locIds = await ResolveProductLocationIdsForSaveAsync(partnerId, groupIds, explicitLocationIds); - return ( - AllScopeBindingHelper.ScopeSpecified, - groupIds.Count > 0 ? AllScopeBindingHelper.ScopeSpecified : AllScopeBindingHelper.ScopeAll, - locIds); - } - - /// - /// 合并 Company(partnerId)、Region(groupIds)、门店(locationIds)并校验存在性。 - /// 有具体门店时以门店为准,不再与 Region/Company 并集。 - /// - private async Task> ResolveProductLocationIdsForSaveAsync( - string? partnerId, - IReadOnlyList groupIds, - IReadOnlyList locationIds) - { - if (LocationScopeBindingHelper.IsAllScopeSentinel(partnerId) - || AllScopeBindingHelper.HasAllScopeSentinelSelection(groupIds) - || AllScopeBindingHelper.HasAllScopeSentinelSelection(locationIds)) - { - throw new UserFriendlyException("适用范围为全选(ALL),无需解析具体门店"); - } - - var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); - if (concreteLocations.Count > 0) - { - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( - _dbContext.SqlSugarClient, concreteLocations); - return concreteLocations; - } - - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + var locScope = await AllScopeBindingHelper.ResolveLabelEntityRegionLocationForSaveAsync( _dbContext.SqlSugarClient, - partnerId, - groupIds, - locationIds); - if (merged.Count == 0) - { - throw new UserFriendlyException("指定适用 Company、Region 或门店时,至少需要匹配到一个有效门店"); - } + input.AvailabilityType, + partnerContext, + mergedRegionIds, + input.LocationIds, + HasProductScopeBinding(input)); - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); - return merged; + return (locScope.AvailabilityType, locScope.AppliedRegionType, locScope.LocationIds); } /// diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TrainingAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TrainingAppService.cs index f7fbcab..c0b2ada 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TrainingAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TrainingAppService.cs @@ -1,3 +1,4 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.Training; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Helpers; @@ -125,6 +126,11 @@ public class TrainingAppService : ApplicationService, ITrainingAppService } } + var allFileEntities = filesByCategory.Values.SelectMany(x => x).ToList(); + var scopeDisplayMap = await TrainingFileScopeHelper.BuildScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + allFileEntities); + var level1 = categories.Where(x => string.IsNullOrWhiteSpace(x.ParentId)).ToList(); var level2Map = categories .Where(x => !string.IsNullOrWhiteSpace(x.ParentId)) @@ -157,7 +163,7 @@ public class TrainingAppService : ApplicationService, ITrainingAppService continue; } - childNodes.Add(MapCategoryNode(l2, fileRows)); + childNodes.Add(MapCategoryNode(l2, fileRows, scopeDisplayMap)); } var l1NameMatch = string.IsNullOrWhiteSpace(keyword) @@ -338,11 +344,21 @@ public class TrainingAppService : ApplicationService, ITrainingAppService } /// - /// 上传培训文件到二级分类 + /// 上传培训文件到二级分类(可同时提交 Company / Region / Location 适用范围) /// /// - /// multipart/form-data:file、categoryId、orderNum。 + /// multipart/form-data:file、categoryId、orderNum,及可选 scope 字段。 /// 支持常见图片与 pdf/doc/docx/xlsx 等,单文件最大 20MB。 + /// + /// 示例 form 字段: + /// - file: 文件 + /// - categoryId: 二级分类 Id + /// - appliedPartnerType: ALL / SPECIFIED + /// - partnerIds: 可重复传值或含 ALL + /// - appliedRegionType: ALL / SPECIFIED + /// - regionIds / groupIds: 可含 ALL + /// - availabilityType 或 appliedLocationType: ALL / SPECIFIED + /// - locationIds: 可含 ALL /// /// 上传表单 /// 文件信息 @@ -352,7 +368,7 @@ public class TrainingAppService : ApplicationService, ITrainingAppService [HttpPost] [Consumes("multipart/form-data")] [Route("/api/app/training/file/upload")] - public async Task UploadFileAsync([FromForm] TrainingFileUploadInputVo input) + public async Task UploadFileAsync([FromForm] TrainingFileUploadInputVo input) { if (input.File is null || input.File.Length <= 0) { @@ -395,6 +411,20 @@ public class TrainingAppService : ApplicationService, ITrainingAppService var now = DateTime.Now; var currentUserId = CurrentUser?.Id?.ToString(); + var hasScopeInput = HasScopeInput(input); + var scope = hasScopeInput + ? await TrainingFileScopeHelper.ResolveScopeForSaveAsync( + _dbContext.SqlSugarClient, + input.AppliedPartnerType, + input.PartnerIds, + input.CompanyIds, + input.AppliedRegionType, + input.RegionIds, + input.GroupIds, + ResolveLocationTypeInput(input), + input.LocationIds) + : null; + var entity = new FlTrainingFileDbEntity { Id = YitIdHelper.NextId().ToString(), @@ -404,9 +434,9 @@ public class TrainingAppService : ApplicationService, ITrainingAppService FileType = ResolveFileType(ext), FileSize = input.File.Length, OrderNum = input.OrderNum, - AppliedPartnerType = AllScopeBindingHelper.ScopeAll, - AppliedRegionType = AllScopeBindingHelper.ScopeAll, - AvailabilityType = AllScopeBindingHelper.ScopeAll, + AppliedPartnerType = scope?.AppliedPartnerType ?? AllScopeBindingHelper.ScopeAll, + AppliedRegionType = scope?.AppliedRegionType ?? AllScopeBindingHelper.ScopeAll, + AvailabilityType = scope?.AvailabilityType ?? AllScopeBindingHelper.ScopeAll, IsDeleted = false, CreationTime = now, CreatorId = currentUserId, @@ -416,28 +446,42 @@ public class TrainingAppService : ApplicationService, ITrainingAppService }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); - - return new TrainingFileUploadOutputDto + if (scope is not null) { - Id = entity.Id, - FileName = entity.FileName, - FileUrl = entity.FileUrl, - FileType = entity.FileType, - FileSize = entity.FileSize - }; + await TrainingFileScopeHelper.SaveScopeAsync( + _dbContext.SqlSugarClient, + entity.Id, + scope, + currentUserId, + now); + } + + var display = await TrainingFileScopeHelper.BuildScopeDisplayAsync(_dbContext.SqlSugarClient, entity); + return MapFileDto(entity, display); } /// - /// 编辑培训文件元数据 + /// 编辑培训文件元数据及适用范围 /// /// /// 示例请求: /// ```json /// { /// "fileName": "操作手册.pdf", - /// "orderNum": 100 + /// "orderNum": 100, + /// "appliedPartnerType": "SPECIFIED", + /// "partnerIds": ["p1"], + /// "appliedRegionType": "ALL", + /// "availabilityType": "SPECIFIED", + /// "locationIds": ["loc1"] /// } /// ``` + /// + /// 参数说明: + /// - fileName / orderNum: 文件元数据 + /// - appliedPartnerType / partnerIds / companyIds: Company 范围,Id 可含 ALL + /// - appliedRegionType / regionIds / groupIds: Region 范围,Id 可含 ALL + /// - availabilityType 或 appliedLocationType / locationIds: Location 范围,Id 可含 ALL /// /// 文件Id /// 文件元数据 @@ -460,7 +504,14 @@ public class TrainingAppService : ApplicationService, ITrainingAppService entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); - return MapFileDto(entity); + + if (HasScopeInput(input)) + { + entity = await ApplyFileScopeAsync(entity, input); + } + + var display = await TrainingFileScopeHelper.BuildScopeDisplayAsync(_dbContext.SqlSugarClient, entity); + return MapFileDto(entity, display); } /// @@ -538,13 +589,15 @@ public class TrainingAppService : ApplicationService, ITrainingAppService } /// - /// 获取培训文件权限范围 + /// 获取培训文件权限范围(兼容独立查询;主路径为 create/update 携带 scope) /// /// 文件Id /// 权限范围 /// 成功 /// 文件不存在 /// 服务器错误 + [HttpGet] + [Route("/api/app/training/file-scope/{id}")] public async Task GetFileScopeAsync(string id) { var entity = await GetFileOrThrowAsync(id); @@ -553,7 +606,7 @@ public class TrainingAppService : ApplicationService, ITrainingAppService } /// - /// 设置培训文件权限范围 + /// 设置培训文件权限范围(兼容独立编辑;主路径为 create/update 携带 scope) /// /// /// 示例请求: @@ -573,9 +626,20 @@ public class TrainingAppService : ApplicationService, ITrainingAppService /// 设置成功 /// 参数无效 /// 服务器错误 + [HttpPut] + [Route("/api/app/training/file-scope/{id}")] public async Task SetFileScopeAsync(string id, TrainingFileScopeInputVo input) { var entity = await GetFileOrThrowAsync(id); + entity = await ApplyFileScopeAsync(entity, input); + var display = await TrainingFileScopeHelper.BuildScopeDisplayAsync(_dbContext.SqlSugarClient, entity); + return MapScopeOutput(display); + } + + private async Task ApplyFileScopeAsync( + FlTrainingFileDbEntity entity, + ITrainingFileScopeInput input) + { var scope = await TrainingFileScopeHelper.ResolveScopeForSaveAsync( _dbContext.SqlSugarClient, input.AppliedPartnerType, @@ -584,7 +648,7 @@ public class TrainingAppService : ApplicationService, ITrainingAppService input.AppliedRegionType, input.RegionIds, input.GroupIds, - input.AvailabilityType, + ResolveLocationTypeInput(input), input.LocationIds); entity.AppliedPartnerType = scope.AppliedPartnerType; @@ -601,29 +665,44 @@ public class TrainingAppService : ApplicationService, ITrainingAppService entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); - var display = await TrainingFileScopeHelper.BuildScopeDisplayAsync(_dbContext.SqlSugarClient, entity); - return MapScopeOutput(display); + return entity; } - private async Task GetCategoryOrThrowAsync(string id) + private static bool HasScopeInput(ITrainingFileScopeInput input) => + !string.IsNullOrWhiteSpace(input.AppliedPartnerType) + || input.PartnerIds is not null + || input.CompanyIds is not null + || !string.IsNullOrWhiteSpace(input.AppliedRegionType) + || input.RegionIds is not null + || input.GroupIds is not null + || !string.IsNullOrWhiteSpace(input.AvailabilityType) + || !string.IsNullOrWhiteSpace(input.AppliedLocationType) + || input.LocationIds is not null; + + private static string? ResolveLocationTypeInput(ITrainingFileScopeInput input) => + string.IsNullOrWhiteSpace(input.AvailabilityType) + ? input.AppliedLocationType + : input.AvailabilityType; + + private async Task GetFileOrThrowAsync(string id) { - var entity = await _dbContext.SqlSugarClient.Queryable() + var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == id && !x.IsDeleted); if (entity is null) { - throw new UserFriendlyException("分类不存在"); + throw new UserFriendlyException("培训文件不存在"); } return entity; } - private async Task GetFileOrThrowAsync(string id) + private async Task GetCategoryOrThrowAsync(string id) { - var entity = await _dbContext.SqlSugarClient.Queryable() + var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == id && !x.IsDeleted); if (entity is null) { - throw new UserFriendlyException("培训文件不存在"); + throw new UserFriendlyException("分类不存在"); } return entity; @@ -642,7 +721,8 @@ public class TrainingAppService : ApplicationService, ITrainingAppService private static TrainingCategoryTreeNodeDto MapCategoryNode( FlTrainingCategoryDbEntity entity, - List files) => + List files, + IReadOnlyDictionary scopeDisplayMap) => new() { Id = entity.Id, @@ -650,10 +730,16 @@ public class TrainingAppService : ApplicationService, ITrainingAppService ParentId = entity.ParentId, OrderNum = entity.OrderNum, Children = new List(), - Files = files.Select(MapFileDto).ToList() + Files = files.Select(file => + { + scopeDisplayMap.TryGetValue(file.Id, out var display); + return MapFileDto(file, display); + }).ToList() }; - private static TrainingFileDto MapFileDto(FlTrainingFileDbEntity entity) => + private static TrainingFileDto MapFileDto( + FlTrainingFileDbEntity entity, + TrainingFileScopeHelper.TrainingFileScopeDisplay? display = null) => new() { Id = entity.Id, @@ -663,9 +749,17 @@ public class TrainingAppService : ApplicationService, ITrainingAppService FileType = entity.FileType, FileSize = entity.FileSize, OrderNum = entity.OrderNum, - AppliedPartnerType = entity.AppliedPartnerType, - AppliedRegionType = entity.AppliedRegionType, - AvailabilityType = entity.AvailabilityType, + AppliedPartnerType = display?.AppliedPartnerType ?? entity.AppliedPartnerType, + Company = display?.Company ?? string.Empty, + PartnerIds = display?.PartnerIds ?? new List(), + CompanyIds = display?.PartnerIds ?? new List(), + AppliedRegionType = display?.AppliedRegionType ?? entity.AppliedRegionType, + Region = display?.Region ?? string.Empty, + RegionIds = display?.RegionIds ?? new List(), + GroupIds = display?.RegionIds ?? new List(), + AvailabilityType = display?.AvailabilityType ?? entity.AvailabilityType, + Location = display?.Location ?? string.Empty, + LocationIds = display?.LocationIds ?? new List(), CreationTime = entity.CreationTime, LastModificationTime = entity.LastModificationTime }; diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs index 0522fcb..7bd1c08 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; @@ -98,12 +98,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService [AllowAnonymous] public virtual async Task LoginAsync(UsAppLoginInputVo input) { - if (_dbConnOptions.EnabledSaasMultiTenancy && !CurrentTenant.Id.HasValue) - { - throw new UserFriendlyException( - "多租户模式下请使用泰额 App 登录接口 /api/app/th-app-auth/login(须传 tenantId),勿使用 us-app-auth。"); - } - + // 泰额 SaaS:由 ThUsAppAuthAppService 覆盖本方法,按邮箱反查租户后登录;此处仅单库模式。 if (string.IsNullOrWhiteSpace(input.Password) || string.IsNullOrWhiteSpace(input.Email)) { throw new UserFriendlyException("请输入合理数据!"); diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs index 2227bcf..19170ec 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs @@ -541,6 +541,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ { var existedBatchId = existed.First().BatchId; var existedTaskIds = existed.Select(x => x.Id).ToList(); + await LabelAlertTimerWriteHelper.TryCreateFromPrintBatchAsync( + _dbContext.SqlSugarClient, + existedBatchId ?? string.Empty, + CurrentUser?.Id?.ToString()); return new UsAppLabelPrintOutputDto { TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty, @@ -689,6 +693,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ } } + await LabelAlertTimerWriteHelper.TryCreateFromPrintBatchAsync( + _dbContext.SqlSugarClient, + batchId, + currentUserId); + return new UsAppLabelPrintOutputDto { TaskId = taskIds.FirstOrDefault() ?? string.Empty, @@ -736,6 +745,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ { var existedBatchId = existed.First().BatchId; var existedTaskIds = existed.Select(x => x.Id).ToList(); + await LabelAlertTimerWriteHelper.TryCreateFromPrintBatchAsync( + _dbContext.SqlSugarClient, + existedBatchId ?? string.Empty, + CurrentUser?.Id?.ToString()); return new UsAppLabelPrintOutputDto { TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty, @@ -846,6 +859,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ } } + await LabelAlertTimerWriteHelper.TryCreateFromPrintBatchAsync( + _dbContext.SqlSugarClient, + batchId, + currentUserId); + return new UsAppLabelPrintOutputDto { TaskId = taskIds.FirstOrDefault() ?? string.Empty, diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/Auth/ThAppLoginInputVo.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/Auth/ThAppLoginInputVo.cs index e9ee4af..d47c2aa 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/Auth/ThAppLoginInputVo.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/Dtos/Auth/ThAppLoginInputVo.cs @@ -3,13 +3,14 @@ using System.ComponentModel.DataAnnotations; namespace FoodLabeling.Th.Application.Contracts.Dtos.Auth; /// -/// 泰额版 App 登录(须指定租户,用户数据在租户独立库) +/// 泰额版 App 登录(用户数据在租户独立库;tenantId 可空,按邮箱反查公司) /// public class ThAppLoginInputVo { - /// 租户 Id(平台主库 yitenant.Id) - [Required] - public Guid TenantId { get; set; } + /// + /// 租户 Id(平台主库 YiTenant.Id)。可空:与 th-web-auth 一致,按邮箱/用户名反查公司租户。 + /// + public Guid? TenantId { get; set; } /// 登录邮箱 [Required] diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/IServices/IThAppAuthAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/IServices/IThAppAuthAppService.cs index 5234d8d..de70769 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/IServices/IThAppAuthAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application.Contracts/IServices/IThAppAuthAppService.cs @@ -10,7 +10,8 @@ namespace FoodLabeling.Th.Application.Contracts.IServices; public interface IThAppAuthAppService : IApplicationService { /// - /// 登录:校验租户存在后,在租户业务库验证账号并签发 Token(Claim 含 TenantId) + /// 登录:校验租户存在后,在租户业务库验证账号并签发 Token(Claim 含 TenantId)。 + /// tenantId 可空,按邮箱/用户名反查公司租户(与 th-web-auth 一致)。 /// Task LoginAsync(ThAppLoginInputVo input); diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Filters/FoodLabelingBusinessTenantActionFilter.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Filters/FoodLabelingBusinessTenantActionFilter.cs index 7d6e5c2..75887aa 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Filters/FoodLabelingBusinessTenantActionFilter.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Filters/FoodLabelingBusinessTenantActionFilter.cs @@ -87,6 +87,7 @@ public class FoodLabelingBusinessTenantActionFilter : IAsyncActionFilter, ITrans { "/th-web-auth", "/th-app-auth", + "/us-app-auth", "/th-multi-tenancy", "/th-tenant-provisioning", "/th-tenant-select", diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThTenantHostDataAccessor.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThTenantHostDataAccessor.cs index fbc1f57..766db3a 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThTenantHostDataAccessor.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/MultiTenancy/ThTenantHostDataAccessor.cs @@ -74,8 +74,10 @@ internal static class ThTenantHostDataAccessor LIMIT 1 """; - var rows = await client.Ado.SqlQueryAsync(sql, new { LoginAccount = normalized }); - if (rows.Count == 0 || !Guid.TryParse(rows[0], out var tenantId)) + // SqlSugar SqlQueryAsync 对单列映射不稳定,使用 DTO + var rows = await client.Ado.SqlQueryAsync(sql, new { LoginAccount = normalized }); + var raw = rows.FirstOrDefault()?.TenantId; + if (string.IsNullOrWhiteSpace(raw) || !Guid.TryParse(raw.Trim(), out var tenantId)) { return null; } @@ -117,6 +119,7 @@ internal static class ThTenantHostDataAccessor { var tenantDbType = tenant.DbType == default ? hostDbType : tenant.DbType; using var tenantClient = TenantBusinessDatabaseAccessor.CreateClient(connectionString, tenantDbType); + tenantClient.Ado.CommandTimeOut = 8; const string sql = """ SELECT CAST(`Id` AS CHAR(36)) AS UserId @@ -130,8 +133,8 @@ internal static class ThTenantHostDataAccessor LIMIT 1 """; - var rows = await tenantClient.Ado.SqlQueryAsync(sql, new { LoginAccount = normalized }); - if (rows.Count > 0) + var rows = await tenantClient.Ado.SqlQueryAsync(sql, new { LoginAccount = normalized }); + if (rows.Count > 0 && !string.IsNullOrWhiteSpace(rows[0].UserId)) { return tenant.Id; } @@ -198,6 +201,15 @@ internal static class ThTenantHostDataAccessor DbType = dbType, ConnectionString = connectionString, IsAutoCloseConnection = true, + MoreSettings = new ConnMoreSettings + { + // 避免反查登录租户时长时间卡在不可达库 + IsAutoRemoveDataCache = true + }, + AopEvents = new AopEvents + { + OnError = _ => { } + }, // 与 DefaultSqlSugarDbContext 一致:主库 menu 等表无 ExtraProperties 列 ConfigureExternalServices = new ConfigureExternalServices { @@ -214,6 +226,20 @@ internal static class ThTenantHostDataAccessor } } } + }, + db => + { + db.Ado.CommandTimeOut = 8; }); } + + private sealed class TenantIdSqlRow + { + public string? TenantId { get; set; } + } + + private sealed class UserIdSqlRow + { + public string? UserId { get; set; } + } } diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThAppAuthAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThAppAuthAppService.cs index 8fbb8c9..f33e2f0 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThAppAuthAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThAppAuthAppService.cs @@ -11,6 +11,7 @@ using FoodLabeling.Th.Application.Contracts.IServices; using Lazy.Captcha.Core; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using SqlSugar; @@ -28,68 +29,106 @@ using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Th.Application.Services; /// -/// 泰额版 App 登录:先解析租户,再在租户独立库校验用户,JWT 写入 TenantId +/// 泰额版 App 登录:解析租户后在租户独立库校验用户,JWT 写入 TenantId /// public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService { private readonly IAccountManager _accountManager; private readonly ISqlSugarRepository _userRepository; private readonly ISugarDbContextProvider _dbContextProvider; + private readonly TenantAdminAccountBootstrapper _adminAccountBootstrapper; private readonly ICaptcha _captcha; private readonly RbacOptions _rbacOptions; private readonly JwtOptions _jwtOptions; private readonly ITenantStore _tenantStore; + private readonly DbConnOptions _dbConnOptions; public ThAppAuthAppService( IAccountManager accountManager, ISqlSugarRepository userRepository, ISugarDbContextProvider dbContextProvider, + TenantAdminAccountBootstrapper adminAccountBootstrapper, ICaptcha captcha, IOptions jwtOptions, IOptions rbacOptions, - ITenantStore tenantStore) + ITenantStore tenantStore, + IOptions dbConnOptions) { _accountManager = accountManager; _userRepository = userRepository; _dbContextProvider = dbContextProvider; + _adminAccountBootstrapper = adminAccountBootstrapper; _captcha = captcha; _jwtOptions = jwtOptions.Value; _rbacOptions = rbacOptions.Value; _tenantStore = tenantStore; + _dbConnOptions = dbConnOptions.Value; } - /// + /// + /// App 登录:校验租户后在租户业务库验证账号并签发 Token(Claim 含 TenantId) + /// + /// + /// 可空:与 th-web-auth/login 一致,按邮箱/用户名反查公司租户 + /// (开通邮箱 fl_th_tenant_admin_credential 或各租户业务库 user 表)。 + /// + /// 示例(可不传 tenantId): + /// ```json + /// { "email": "mai@123.com", "password": "123456" } + /// ``` + /// + /// 登录参数 + /// Token、RefreshToken、租户信息与绑定门店 + /// 登录成功 + /// 参数缺失、验证码错误或账号密码错误 + /// 服务器错误 [AllowAnonymous] [HttpPost("th-app-auth/login")] public virtual async Task LoginAsync(ThAppLoginInputVo input) { - if (input.TenantId == Guid.Empty - || string.IsNullOrWhiteSpace(input.Password) + if (string.IsNullOrWhiteSpace(input.Password) || string.IsNullOrWhiteSpace(input.Email)) { - throw new UserFriendlyException("请输入租户、邮箱与密码!"); + throw new UserFriendlyException("请输入邮箱与密码!"); } ValidationImageCaptcha(input.Uuid, input.Code); - var tenantConfig = await TenantResolveHelper.ResolveTenantAsync( - _tenantStore, CurrentTenant, input.TenantId); + var tenantId = await ResolveTenantIdForLoginAsync(input); + if (!tenantId.HasValue || tenantId.Value == Guid.Empty) + { + throw new UserFriendlyException("登录失败:未找到该邮箱对应的公司,请确认邮箱或联系管理员"); + } - UserAggregateRoot user; - List locations; - using (CurrentTenant.Change(input.TenantId, tenantConfig.Name)) + if (ThWebPlatformLoginHelper.IsBusinessTenantLogin(tenantId, null)) { - user = await FindActiveUserByEmailAsync(input.Email.Trim()) + await EnsureNotHostPlatformAccountAsync(input.Email); + } + + var tenantConfig = await TenantResolveHelper.ResolveTenantAsync( + _tenantStore, CurrentTenant, tenantId.Value); + + // 与 th-web-auth 对齐:开通邮箱已在主库凭据中、但租户库仍为 Seed admin 时,登录前补同步 + await EnsureTenantAdminLoginSyncedAsync(tenantId.Value, input.Email.Trim()); + + // 优先直连租户库查用户,避免 CurrentTenant.Change 后仓储仍连主库导致「邮箱不存在」 + var user = await FindActiveUserByEmailInTenantDbAsync(tenantId.Value, input.Email.Trim()) ?? throw new UserFriendlyException("登录失败!邮箱不存在!"); - if (!UserPasswordHelper.VerifyPlainPassword(user, input.Password)) - { - throw new UserFriendlyException(UserConst.Login_Error); - } + if (!UserPasswordHelper.VerifyPlainPassword(user, input.Password) + && !user.JudgePassword(input.Password)) + { + throw new UserFriendlyException(UserConst.Login_Error); + } + List locations; + List roleCodes; + using (CurrentTenant.Change(tenantId.Value, tenantConfig.Name)) + { locations = await LoadBoundLocationsAsync(user.Id); + roleCodes = await LoadUserRoleCodesAsync(user.Id); } - var accessToken = CreateAppAccessToken(user, input.TenantId); + var accessToken = CreateAppAccessToken(user, tenantId.Value, roleCodes); var refreshToken = _accountManager.CreateRefreshToken(user.Id); // 不发布 LoginEvent:事件在 UoW 完成时于「当前连接」写 LoginLog;泰额 App 校验在租户库, @@ -99,7 +138,7 @@ public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService { Token = accessToken, RefreshToken = refreshToken, - TenantId = input.TenantId, + TenantId = tenantId.Value, TenantName = tenantConfig.Name ?? string.Empty, Locations = locations }; @@ -126,6 +165,95 @@ public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService } } + /// + /// 解析登录租户:显式 tenantId 优先;否则按邮箱反查(与 th-web-auth 对齐)。 + /// + private async Task ResolveTenantIdForLoginAsync(ThAppLoginInputVo input) + { + if (input.TenantId.HasValue && input.TenantId.Value != Guid.Empty) + { + if (input.TenantId.Value == ThWebPlatformLoginHelper.DefaultTenantId) + { + return await ThLoginTenantResolver.TryResolveCompanyTenantIdByLoginAsync( + _dbConnOptions, + input.Email, + Logger); + } + + return input.TenantId.Value; + } + + return await ThLoginTenantResolver.TryResolveCompanyTenantIdByLoginAsync( + _dbConnOptions, + input.Email, + Logger); + } + + /// + /// 业务租户登录前校验:主库平台邮箱账号不得落入公司业务库。 + /// + private async Task EnsureNotHostPlatformAccountAsync(string email) + { + if (!ThWebPlatformLoginHelper.IsPlausiblePlatformEmail(email)) + { + return; + } + + using (CurrentTenant.Change(null)) + { + var hostUser = await FindActiveUserByEmailAsync(email.Trim()); + if (hostUser != null) + { + throw new UserFriendlyException(ThWebPlatformLoginHelper.PlatformAccountMustUseDefaultMessage); + } + } + } + + /// + /// 开通邮箱已写入主库凭据、但业务库仍为 Seed 的 admin 时,登录前补同步一次(与 th-web-auth 一致)。 + /// + private async Task EnsureTenantAdminLoginSyncedAsync(Guid tenantId, string loginAccount) + { + if (!ThWebPlatformLoginHelper.IsPlausiblePlatformEmail(loginAccount)) + { + return; + } + + var credentialAccount = await _adminAccountBootstrapper.GetProvisionedLoginAccountAsync(tenantId); + if (string.IsNullOrWhiteSpace(credentialAccount) + || !string.Equals(credentialAccount, loginAccount.Trim(), StringComparison.OrdinalIgnoreCase)) + { + return; + } + + try + { + using (CurrentTenant.Change(tenantId)) + { + var existing = await FindActiveUserByEmailAsync(loginAccount); + if (existing != null) + { + await _adminAccountBootstrapper.EnsureTenantAdminRoleAsync(tenantId); + return; + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "租户 {TenantId} App 登录前探测用户失败,尝试同步开通邮箱", tenantId); + } + + try + { + await _adminAccountBootstrapper.ApplyLoginEmailAsync(tenantId); + await _adminAccountBootstrapper.EnsureTenantAdminRoleAsync(tenantId); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "租户 {TenantId} App 登录前同步管理员邮箱失败", tenantId); + } + } + private void ValidationImageCaptcha(string? uuid, string? code) { if (!_rbacOptions.EnableCaptcha) @@ -142,7 +270,9 @@ public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService private async Task FindActiveUserByEmailAsync(string email) { var normalized = email.Trim().ToLowerInvariant(); - var users = await _userRepository._DbQueryable + // Change 租户后必须用 Provider 取当前库连接;仓储可能仍缓存主库连接 + var db = (await _dbContextProvider.GetDbContextAsync()).SqlSugarClient; + var users = await db.Queryable() .Where(u => !u.IsDeleted && u.State) .Where(u => (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) || @@ -154,7 +284,39 @@ public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService ?? users.FirstOrDefault(); } - private string CreateAppAccessToken(UserAggregateRoot user, Guid tenantId) + /// + /// 直连租户业务库查用户(绕过 CurrentTenant 连接切换不稳定问题)。 + /// + private async Task FindActiveUserByEmailInTenantDbAsync(Guid tenantId, string email) + { + var tenant = await ThTenantHostDataAccessor.LoadTenantAsync(_dbConnOptions, tenantId); + if (tenant is null || string.IsNullOrWhiteSpace(tenant.TenantConnectionString)) + { + return null; + } + + var dbType = tenant.DbType == default + ? (_dbConnOptions.DbType ?? SqlSugar.DbType.MySql) + : tenant.DbType; + using var tenantDb = TenantBusinessDatabaseAccessor.CreateClient( + tenant.TenantConnectionString.Trim(), + dbType); + tenantDb.Ado.CommandTimeOut = 15; + + var normalized = email.Trim().ToLowerInvariant(); + var users = await tenantDb.Queryable() + .Where(u => !u.IsDeleted && u.State) + .Where(u => + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) || + SqlFunc.ToLower(u.UserName) == normalized) + .ToListAsync(); + return users.FirstOrDefault(u => + u.Email != null && + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase)) + ?? users.FirstOrDefault(); + } + + private string CreateAppAccessToken(UserAggregateRoot user, Guid tenantId, IReadOnlyList roleCodes) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); @@ -174,6 +336,21 @@ public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService claims.Add(new Claim(AbpClaimTypes.Email, user.Email)); } + var isAdmin = roleCodes.Any(r => + string.Equals(r, UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)); + if (isAdmin || string.Equals(user.UserName, UserConst.Admin, StringComparison.OrdinalIgnoreCase)) + { + claims.Add(new Claim(TokenTypeConst.Permission, UserConst.AdminPermissionCode)); + claims.Add(new Claim(TokenTypeConst.Roles, UserConst.AdminRolesCode)); + } + else + { + foreach (var role in roleCodes.Where(r => !string.IsNullOrWhiteSpace(r)).Distinct(StringComparer.OrdinalIgnoreCase)) + { + claims.Add(new Claim(AbpClaimTypes.Role, role)); + } + } + var token = new JwtSecurityToken( _jwtOptions.Issuer, _jwtOptions.Audience, @@ -185,6 +362,29 @@ public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService return new JwtSecurityTokenHandler().WriteToken(token); } + private async Task> LoadUserRoleCodesAsync(Guid userId) + { + var db = (await _dbContextProvider.GetDbContextAsync()).SqlSugarClient; + var rows = await db.Ado.SqlQueryAsync( + """ + SELECT r.RoleCode + FROM UserRole ur + INNER JOIN Role r ON ur.RoleId = r.Id + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 + """, + new { UserId = userId }); + return rows + .Select(r => r.RoleCode?.Trim()) + .Where(r => !string.IsNullOrWhiteSpace(r)) + .Cast() + .ToList(); + } + + private sealed class RoleCodeRow + { + public string? RoleCode { get; set; } + } + private async Task> LoadBoundLocationsAsync(Guid userId) { var db = (await _dbContextProvider.GetDbContextAsync()).SqlSugarClient; diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs index 4ce6645..5bd5537 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs @@ -107,7 +107,10 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService return platformLogin; } - var companyTenantId = await TryResolveCompanyTenantIdByLoginAsync(input.UserName); + var companyTenantId = await ThLoginTenantResolver.TryResolveCompanyTenantIdByLoginAsync( + _dbConnOptions, + input.UserName, + Logger); if (companyTenantId.HasValue) { input.TenantId = companyTenantId; @@ -203,40 +206,6 @@ public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService } /// - /// 未选公司时:先查开通管理员凭据,再扫描各租户业务库 user(Team Member 等)。 - /// - private async Task TryResolveCompanyTenantIdByLoginAsync(string? userName) - { - if (string.IsNullOrWhiteSpace(userName)) - { - return null; - } - - try - { - if (ThWebPlatformLoginHelper.IsPlausiblePlatformEmail(userName)) - { - var fromCredential = await ThTenantHostDataAccessor.FindActiveTenantIdByLoginAccountAsync( - _dbConnOptions, - userName); - if (fromCredential.HasValue) - { - return fromCredential; - } - } - - return await ThTenantHostDataAccessor.FindActiveTenantIdByBusinessUserLoginAsync( - _dbConnOptions, - userName); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "按登录账号反查公司租户失败:{UserName}", userName); - return null; - } - } - - /// /// 业务租户登录前校验:主库平台邮箱账号不得落入公司业务库。 /// 仅拦截邮箱形态(与 TryPlatformLoginAsync 一致);租户默认 UserName=admin 与主库 admin 同名时允许走租户库。 /// diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json index fb3aaf7..d42b098 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json @@ -67,7 +67,7 @@ "DbList": [ "Sqlite", "Mysql", "Sqlserver", "Oracle", "PostgreSQL" ], "DbConnOptions": { - "Url": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-host;uid=javateam;pwd=javateam2026;CharSet=utf8mb4;", + "Url": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-host;uid=netteam;pwd=netteam;CharSet=utf8mb4;", // 高权限:仅建库/授权;租户连接串仍用 FoodLabeling:TenantDatabase 的 netteam "AdminConnectionString": "server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;port=3306;database=antis-foodlabeling-host;uid=javateam;pwd=javateam2026;CharSet=utf8mb4;", "DbType": "MySql", diff --git a/项目相关文档/培训接口文档.md b/项目相关文档/培训接口文档.md index 3ac4eb7..bd2fd5e 100644 --- a/项目相关文档/培训接口文档.md +++ b/项目相关文档/培训接口文档.md @@ -1,9 +1,11 @@ -# 培训接口文档(泰额版后端) +# 培训接口文档(泰额版) > 模块:培训 / 资料中心 -> 范围:仅泰额版后端;需使用**租户业务库**(非 `antis-foodlabeling-host`) -> 认证:管理端 / APP 接口均需登录 Token(`Authorization: Bearer {token}`) -> 约定:JSON 字段一般为 camelCase +> 范围:泰额版后端(租户业务库,非 `antis-foodlabeling-host`) +> 认证:`Authorization: Bearer {token}`;业务请求建议带 `__tenant: {tenantId}` +> JSON:camelCase +> Base URL 示例:`http://127.0.0.1:19002`(以实际部署为准) +> 更新日期:2026-08-07 --- @@ -13,11 +15,27 @@ |------|------| | 一级分类 | `parentId` 为空 | | 二级分类 | `parentId` 指向一级分类 Id;**仅两级** | -| 培训文件 | 只能挂在**二级分类**下 | -| 权限 | 落在**文件**上:Company / Region / Location,各为 `ALL` 或 `SPECIFIED` | -| Region | 对应 `fl_group`(`groupId` / `regionIds`) | -| Company | 对应 `fl_partner`(`partnerIds` / `companyIds` 等价) | -| Location | 对应 `location` | +| 培训文件 | **只能挂在二级分类下** | +| 文件权限 | 落在**文件**上:Company / Region / Location,各为 `ALL` 或 `SPECIFIED` | +| Company | `fl_partner`;字段 `partnerIds` / `companyIds` 等价 | +| Region | `fl_group`;字段 `regionIds` / `groupIds` 等价 | +| Location | `location`;字段 `locationIds` | + +### 权限约定(推荐做法) + +**主路径:上传 / 编辑文件时直接传公司、区域、门店范围**(可传 `ALL` 或具体多值 Id)。 +独立「编辑文件权限」接口仅作兼容保留,前端可不使用。 + +| 维度 | 类型字段 | Id 数组 | 说明 | +|------|----------|---------|------| +| Company | `appliedPartnerType` | `partnerIds` / `companyIds` | `ALL` 或 `SPECIFIED` + Guid 列表(可含哨兵 `"ALL"`) | +| Region | `appliedRegionType` | `regionIds` / `groupIds` | 同上 | +| Location | `availabilityType`(别名 `appliedLocationType`) | `locationIds` | 同上 | + +- 上传时**不传**任何 scope 字段 → 默认三维度均为 `ALL` +- 编辑时**不传**任何 scope 字段 → **不改**原权限(仅改文件名/排序) +- 编辑时传入任一 scope 字段 → 整套权限按入参覆盖保存 +- `SPECIFIED` 且仅选当前上下文「恰好全集」时,回显保持具体 Guid / 声明类型,不误折成 `["ALL"]`(与标签模板等一致) ### 文件存储 @@ -28,13 +46,13 @@ | 库中 `fileUrl` | `/training/{存储文件名}` | - 单文件最大 **20MB** -- 支持扩展名:`.jpg/.jpeg/.png/.webp/.gif/.bmp`、`.pdf/.doc/.docx/.xls/.xlsx/.ppt/.pptx/.txt/.csv` +- 扩展名:`.jpg/.jpeg/.png/.webp/.gif/.bmp`、`.pdf/.doc/.docx/.xls/.xlsx/.ppt/.pptx/.txt/.csv` - `fileType`:`image` / `doc` / `other` -### 建表 SQL +### 建表 - 脚本:`module/food-labeling-us/scripts/fl_training.sql` -- **在每个租户业务库执行**;新租户创建时会自动执行该脚本 +- 在每个**租户业务库**执行;新租户开通时会自动执行 --- @@ -48,14 +66,15 @@ | 新增分类 | POST | `/api/app/training/category` | | 编辑分类 | PUT | `/api/app/training/category/{id}` | | 删除分类 | DELETE | `/api/app/training/category/{id}` | -| 上传文件 | POST | `/api/app/training/file/upload` | -| 编辑文件 | PUT | `/api/app/training/file/{id}` | -| 删除文件 | DELETE | `/api/app/training/file/{id}` | +| 上传文件(含权限) | POST | `/api/app/training/file/upload` | +| 编辑文件(含权限) | PUT | `/api/app/training/{id}/file` | +| 删除文件 | DELETE | `/api/app/training/{id}/file` | | 文件排序 | PUT | `/api/app/training/sort-files` | -| 获取文件权限 | GET | `/api/app/training/file-scope/{id}` | -| 设置文件权限 | PUT | `/api/app/training/file-scope/{id}` | +| 获取文件权限(兼容) | GET | `/api/app/training/file-scope/{id}` | +| 设置文件权限(兼容) | PUT | `/api/app/training/file-scope/{id}` | -> 除上传接口为显式路由外,其余为 ABP 约定路由。若与 Swagger 不一致,以 Swagger 为准。 +> 上传、`file-scope` 为显式路由;其余多为 ABP 约定路由。以 Swagger 为准。 +> 另有约定路径 `GET/PUT /api/app/training/{id}/file-scope`,与 `file-scope/{id}` 等价兼容。 ### 2.2 APP `UsAppTrainingAppService` @@ -79,36 +98,44 @@ | locationId | string | 否 | 按门店过滤可见文件;不传则不过滤权限 | | includeFiles | bool | 否 | 是否返回文件,默认 `true` | -**响应示例** +**响应要点**:一级 `children` 为二级;二级 `files` 为 `TrainingFileDto`(含完整 scope 回显字段)。 ```json [ { "id": "1001", - "categoryName": "Training", + "categoryName": "分类1", "parentId": null, "orderNum": 100, "children": [ { "id": "1002", - "categoryName": "BOH", + "categoryName": "子1", "parentId": "1001", "orderNum": 90, "children": [], "files": [ { - "id": "2001", + "id": "23148558702612480", "categoryId": "1002", - "fileName": "手册.pdf", - "fileUrl": "/training/20260804120000_123.pdf", + "fileName": "学情测评试题.pdf", + "fileUrl": "/training/20260807120000_xxx.pdf", "fileType": "doc", "fileSize": 102400, "orderNum": 100, "appliedPartnerType": "ALL", - "appliedRegionType": "ALL", - "availabilityType": "ALL", - "creationTime": "2026-08-04T12:00:00", - "lastModificationTime": "2026-08-04T12:00:00" + "company": "All Companies", + "partnerIds": ["ALL"], + "companyIds": ["ALL"], + "appliedRegionType": "SPECIFIED", + "region": "武侯区", + "regionIds": ["3a22e2f6-0679-9a5b-19b4-fc6db2d92dcc"], + "groupIds": ["3a22e2f6-0679-9a5b-19b4-fc6db2d92dcc"], + "availabilityType": "SPECIFIED", + "location": "分店2", + "locationIds": ["3a22e2f6-e944-2038-ffd8-fc82d9d388ac"], + "creationTime": "2026-08-07T12:00:00", + "lastModificationTime": "2026-08-07T12:00:00" } ] } @@ -124,8 +151,6 @@ `POST /api/app/training/category` -**Body** - ```json { "categoryName": "Training", @@ -136,27 +161,13 @@ | 字段 | 说明 | |------|------| -| categoryName | 分类名称,同级不可重名 | -| parentId | 空/不传 = 一级;传一级 Id = 二级 | -| orderNum | 排序,数值越大越靠前(按实现排序) | +| categoryName | 必填;同级不可重名 | +| parentId | 空 = 一级;传一级 Id = 二级 | +| orderNum | 排序 | -**响应**:`TrainingCategoryGetOutputDto` - -```json -{ - "id": "1001", - "categoryName": "Training", - "parentId": null, - "orderNum": 100, - "creationTime": "2026-08-04T12:00:00", - "lastModificationTime": "2026-08-04T12:00:00" -} -``` +**响应**:`TrainingCategoryGetOutputDto`(`id` / `categoryName` / `parentId` / `orderNum` / 时间字段) -**常见错误** -- 分类名称为空 -- 父级不存在 / 在二级下再建子级 -- 同级名称已存在 +常见错误:名称为空、父级不存在、在二级下再建子级、同级重名。 --- @@ -164,8 +175,6 @@ `PUT /api/app/training/category/{id}` -**Body** - ```json { "categoryName": "Training(更新)", @@ -173,7 +182,7 @@ } ``` -> 不可通过本接口改层级(`parentId` 不可改)。 +不可改层级(`parentId` 不可改)。 --- @@ -181,13 +190,11 @@ `DELETE /api/app/training/category/{id}` -软删除。规则: -- 一级下仍有二级 → 不可删 -- 二级下仍有文件 → 不可删 +软删除。规则:一级下仍有二级不可删;二级下仍有文件不可删。 --- -### 3.5 上传文件 +### 3.5 上传文件(主路径,含权限) `POST /api/app/training/file/upload` `Content-Type: multipart/form-data` @@ -197,43 +204,96 @@ | file | file | 是 | 文件本体 | | categoryId | string | 是 | **二级**分类 Id | | orderNum | int | 否 | 排序 | +| appliedPartnerType | string | 否 | `ALL` / `SPECIFIED` | +| partnerIds | string[] | 否 | 可重复传多个 form 字段;可含 `ALL` | +| companyIds | string[] | 否 | 同 partnerIds | +| appliedRegionType | string | 否 | `ALL` / `SPECIFIED` | +| regionIds | string[] | 否 | 可含 `ALL` | +| groupIds | string[] | 否 | 同 regionIds | +| availabilityType | string | 否 | Location:`ALL` / `SPECIFIED` | +| appliedLocationType | string | 否 | `availabilityType` 别名 | +| locationIds | string[] | 否 | 可含 `ALL` | + +**curl 示例(全 ALL)** -**响应** +```bash +curl -X POST "http://127.0.0.1:19002/api/app/training/file/upload" \ + -H "Authorization: Bearer " \ + -H "__tenant: " \ + -F "file=@./手册.pdf" \ + -F "categoryId=<二级分类Id>" \ + -F "orderNum=100" \ + -F "appliedPartnerType=ALL" \ + -F "partnerIds=ALL" \ + -F "appliedRegionType=ALL" \ + -F "regionIds=ALL" \ + -F "availabilityType=ALL" \ + -F "locationIds=ALL" +``` -```json -{ - "id": "2001", - "fileName": "手册.pdf", - "fileUrl": "/training/20260804120000_123.pdf", - "fileType": "doc", - "fileSize": 102400 -} +**curl 示例(指定公司 + 门店)** + +```bash +curl -X POST "http://127.0.0.1:19002/api/app/training/file/upload" \ + -H "Authorization: Bearer " \ + -H "__tenant: " \ + -F "file=@./手册.pdf" \ + -F "categoryId=<二级分类Id>" \ + -F "appliedPartnerType=SPECIFIED" \ + -F "partnerIds=" \ + -F "appliedRegionType=SPECIFIED" \ + -F "regionIds=" \ + -F "availabilityType=SPECIFIED" \ + -F "locationIds=" \ + -F "locationIds=" ``` -> 新上传文件默认权限:Company / Region / Location 均为 `ALL`。 +**响应**:完整 `TrainingFileDto`(含 scope 回显,结构见 3.1)。 --- -### 3.6 编辑文件元数据 +### 3.6 编辑文件(主路径,含权限) -`PUT /api/app/training/file/{id}` +`PUT /api/app/training/{id}/file` ```json { "fileName": "操作手册.pdf", - "orderNum": 100 + "orderNum": 100, + "appliedPartnerType": "SPECIFIED", + "partnerIds": ["3a22e2f5-a785-ba39-9847-de45bdd47e50"], + "companyIds": ["3a22e2f5-a785-ba39-9847-de45bdd47e50"], + "appliedRegionType": "ALL", + "regionIds": ["ALL"], + "groupIds": ["ALL"], + "availabilityType": "SPECIFIED", + "locationIds": ["3a22e2f6-e944-2038-ffd8-fc82d9d388ac"] } ``` -不更换物理文件,仅改展示名与排序。 +| 字段 | 说明 | +|------|------| +| fileName | 必填;展示名(不换物理文件) | +| orderNum | 排序 | +| scope 各字段 | 见第 1 节;**只要传了任一 scope 字段即整套覆盖**;全不传则保持原权限 | + +**响应**:完整 `TrainingFileDto`。 + +```bash +curl -X PUT "http://127.0.0.1:19002/api/app/training//file" \ + -H "Authorization: Bearer " \ + -H "__tenant: " \ + -H "Content-Type: application/json" \ + -d "{\"fileName\":\"学情测评试题.pdf\",\"orderNum\":100,\"appliedPartnerType\":\"ALL\",\"partnerIds\":[\"ALL\"],\"appliedRegionType\":\"ALL\",\"availabilityType\":\"ALL\",\"locationIds\":[\"ALL\"]}" +``` --- ### 3.7 删除文件 -`DELETE /api/app/training/file/{id}` +`DELETE /api/app/training/{id}/file` -软删除,并清理权限关联行。 +软删除,并清理 scope 关联行。 --- @@ -244,156 +304,139 @@ ```json { "items": [ - { "id": "2001", "orderNum": 100 }, - { "id": "2002", "orderNum": 90 } + { "id": "23148558702612480", "orderNum": 100 }, + { "id": "23148558702612481", "orderNum": 90 } ] } ``` --- -### 3.9 获取文件权限 +### 3.9 获取 / 设置文件权限(兼容,非推荐主路径) -`GET /api/app/training/file-scope/{id}` +| 方法 | 路由 | +|------|------| +| GET | `/api/app/training/file-scope/{id}` | +| PUT | `/api/app/training/file-scope/{id}` | + +**推荐**:权限在 **上传 / 编辑文件** 中一并提交,不必单独调本接口。 +本接口与 create/update 共用同一套 scope 保存逻辑;PUT body 与 scope 字段相同(无 `fileName`)。 -**响应示例** +**GET 响应**(`TrainingFileScopeOutputDto`) ```json { "appliedPartnerType": "SPECIFIED", - "company": "公司A", - "partnerIds": ["p1"], - "companyIds": ["p1"], - "appliedRegionType": "ALL", - "region": "All Regions", - "regionIds": [], - "groupIds": [], + "company": "成都分店", + "partnerIds": ["3a22e2f5-a785-ba39-9847-de45bdd47e50"], + "companyIds": ["3a22e2f5-a785-ba39-9847-de45bdd47e50"], + "appliedRegionType": "SPECIFIED", + "region": "武侯区", + "regionIds": ["3a22e2f6-0679-9a5b-19b4-fc6db2d92dcc"], + "groupIds": ["3a22e2f6-0679-9a5b-19b4-fc6db2d92dcc"], "availabilityType": "SPECIFIED", - "location": "门店1", - "locationIds": ["loc1"] + "location": "分店2", + "locationIds": ["3a22e2f6-e944-2038-ffd8-fc82d9d388ac"] } ``` ---- - -### 3.10 设置文件权限 - -`PUT /api/app/training/file-scope/{id}` +**PUT body 示例** ```json { - "appliedPartnerType": "SPECIFIED", - "partnerIds": ["p1"], - "companyIds": ["p1"], - "appliedRegionType": "ALL", - "regionIds": [], - "groupIds": [], + "appliedPartnerType": "ALL", + "partnerIds": ["ALL"], + "appliedRegionType": "SPECIFIED", + "regionIds": ["3a22e2f6-0679-9a5b-19b4-fc6db2d92dcc"], "availabilityType": "SPECIFIED", - "locationIds": ["loc1"] + "locationIds": ["3a22e2f6-e944-2038-ffd8-fc82d9d388ac"] } ``` -| 字段 | 说明 | -|------|------| -| appliedPartnerType | `ALL` / `SPECIFIED` | -| partnerIds / companyIds | 公司 Id,二者等价,合并去重 | -| appliedRegionType | `ALL` / `SPECIFIED` | -| regionIds / groupIds | 区域 Id(`fl_group.Id`),二者等价 | -| availabilityType | `ALL` / `SPECIFIED`(门店维度) | -| locationIds | 门店 Id | - -**过滤规则(APP / 带 locationId 的树)** -三个维度均为:`ALL` **或** `SPECIFIED` 且命中该门店对应的公司 / 区域 / 门店;三维度 **同时满足** 才可见。 - --- -## 4. APP 接口详情 +## 4. APP 接口 -### 4.1 按门店获取可见分类树 +### 4.1 门店可见分类树 `GET /api/app/us-app-training/tree` -**Query** - | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| -| locationId | string | 是 | 当前门店 Id | +| locationId | string | 是 | 当前门店 Id;校验用户可访问该门店 | | keyword | string | 否 | 匹配分类名或文件名 | -**说明** -- 校验当前用户是否可访问该门店 -- 仅返回对当前门店有权限的文件;无可见文件的空分支会按实现裁剪 -- 响应结构与管理端分类树相同:`List` - ---- - -## 5. 公共 DTO 字段说明 +按门店过滤文件:Company / Region / Location 三维度 `ALL` 或 `SPECIFIED` 命中该门店才可见。 +响应结构同管理端分类树(`includeFiles=true`)。 -### TrainingCategoryTreeNodeDto +```bash +curl -G "http://127.0.0.1:19002/api/app/us-app-training/tree" \ + --data-urlencode "locationId=" \ + -H "Authorization: Bearer " \ + -H "__tenant: " +``` -| 字段 | 类型 | 说明 | -|------|------|------| -| id | string | 分类 Id | -| categoryName | string | 名称 | -| parentId | string? | 父级;一级为 null | -| orderNum | int | 排序 | -| children | array | 子分类 | -| files | array | 文件列表(一般挂在二级) | +--- -### TrainingFileDto +## 5. TrainingFileDto 字段说明 -| 字段 | 类型 | 说明 | -|------|------|------| -| id | string | 文件 Id | -| categoryId | string | 二级分类 Id | -| fileName | string | 展示文件名 | -| fileUrl | string | 相对路径,如 `/training/xxx.pdf` | -| fileType | string | `image` / `doc` / `other` | -| fileSize | long | 字节 | -| orderNum | int | 排序 | -| appliedPartnerType | string | ALL / SPECIFIED | -| appliedRegionType | string | ALL / SPECIFIED | -| availabilityType | string | ALL / SPECIFIED | -| creationTime | datetime | 创建时间 | -| lastModificationTime | datetime? | 最后修改时间 | +| 字段 | 说明 | +|------|------| +| id | 文件 Id | +| categoryId | 所属二级分类 | +| fileName / fileUrl / fileType / fileSize | 文件信息 | +| orderNum | 排序 | +| appliedPartnerType | Company:`ALL` / `SPECIFIED` | +| company | Company 展示文案 | +| partnerIds / companyIds | Company Id 列表(可含 `ALL`) | +| appliedRegionType | Region:`ALL` / `SPECIFIED` | +| region | Region 展示文案 | +| regionIds / groupIds | Region Id 列表 | +| availabilityType | Location:`ALL` / `SPECIFIED` | +| location | Location 展示文案 | +| locationIds | 门店 Id 列表 | +| creationTime / lastModificationTime | 时间 | --- -## 6. curl 示例 +## 6. 联调检查清单 -```bash -# 分类树 -curl -G "{{baseUrl}}/api/app/training/category-tree" \ - -H "Authorization: Bearer {{token}}" \ - --data-urlencode "includeFiles=true" - -# 新增一级分类 -curl -X POST "{{baseUrl}}/api/app/training/category" \ - -H "Authorization: Bearer {{token}}" \ - -H "Content-Type: application/json" \ - -d '{"categoryName":"Training","parentId":null,"orderNum":100}' +- [ ] 分类仅两级;文件只能挂二级 +- [ ] 上传可不传 scope → 默认三维度 ALL +- [ ] 上传/编辑传 `ALL` 或具体多值 Guid → 库表 type 字段 + 关联表正确 +- [ ] 分类树 `files[]` 含完整 scope 回显 +- [ ] `PUT .../file` 不传 scope 时不改权限;传了则覆盖 +- [ ] `GET/PUT /api/app/training/file-scope/{id}` 不再 404(兼容路径) +- [ ] APP `us-app-training/tree` 按 locationId 过滤可见文件 +- [ ] 文件 ≤20MB、扩展名合法 -# 上传文件 -curl -X POST "{{baseUrl}}/api/app/training/file/upload" \ - -H "Authorization: Bearer {{token}}" \ - -F "file=@./手册.pdf" \ - -F "categoryId=1002" \ - -F "orderNum=100" +查库(租户业务库): + +```sql +SELECT Id, FileName, AppliedPartnerType, AppliedRegionType, AvailabilityType +FROM fl_training_file WHERE IsDeleted = 0; -# APP 门店树 -curl -G "{{baseUrl}}/api/app/us-app-training/tree" \ - -H "Authorization: Bearer {{token}}" \ - --data-urlencode "locationId=loc1" +SELECT * FROM fl_training_file_partner WHERE TrainingFileId = ''; +SELECT * FROM fl_training_file_region WHERE TrainingFileId = ''; +SELECT * FROM fl_training_file_location WHERE TrainingFileId = ''; ``` --- ## 7. 相关代码 -| 项 | 路径 | -|----|------| +| 说明 | 路径 | +|------|------| | 管理端服务 | `FoodLabeling.Application/Services/TrainingAppService.cs` | | APP 服务 | `FoodLabeling.Application/Services/UsAppTrainingAppService.cs` | -| 权限 Helper | `FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs` | -| SQL | `module/food-labeling-us/scripts/fl_training.sql` | -| 新租户迁移 | `FoodLabelingTenantMigrationScriptNames.Training` | +| Scope 辅助 | `FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs` | +| 建表脚本 | `module/food-labeling-us/scripts/fl_training.sql` | + +--- + +## 8. 变更记录 + +| 日期 | 说明 | +|------|------| +| 2026-08-07 | 重写文档:权限并入上传/编辑;file-scope 改为兼容路径;补全 TrainingFileDto scope 回显与 curl 示例 | +| (历史) | 初版:分类树、文件 CRUD、独立 file-scope、APP tree | -- libgit2 0.21.4