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