From b4c5ebc9260e6611b3b2c2d9b83a2d18b8a8edd8 Mon Sep 17 00:00:00 2001 From: 18631081161 <2088094923@qq.com> Date: Fri, 16 Jan 2026 17:19:58 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=86=E8=8A=82=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 138 +++ .../requirements.md | 47 + .../tasks.md | 87 ++ .../RequiredFieldValidationPropertyTests.cs | 110 +-- .../VisibilityFilterPropertyTests.cs | 878 ++++++++++++++++++ .../Services/ReportingServiceTests.cs | 26 +- .../Controllers/AllocationsController.cs | 88 +- .../Controllers/ReportsController.cs | 35 +- .../Implementations/AllocationService.cs | 48 + .../Implementations/OrganizationService.cs | 36 + .../Implementations/ReportingService.cs | 59 +- .../Services/Interfaces/IAllocationService.cs | 13 + .../Interfaces/IOrganizationService.cs | 10 + .../Services/Interfaces/IReportingService.cs | 18 +- .../views/allocations/AllocationReport.vue | 77 +- 15 files changed, 1478 insertions(+), 192 deletions(-) create mode 100644 .kiro/specs/consumption-report-visibility-filter/design.md create mode 100644 .kiro/specs/consumption-report-visibility-filter/requirements.md create mode 100644 .kiro/specs/consumption-report-visibility-filter/tasks.md create mode 100644 src/MilitaryTrainingManagement.Tests/Properties/VisibilityFilterPropertyTests.cs diff --git a/.kiro/specs/consumption-report-visibility-filter/design.md b/.kiro/specs/consumption-report-visibility-filter/design.md new file mode 100644 index 0000000..bbe0d7a --- /dev/null +++ b/.kiro/specs/consumption-report-visibility-filter/design.md @@ -0,0 +1,138 @@ +# Design Document + +## Overview + +本设计实现营部及以下级别账号的消耗上报可见性过滤功能。核心思路是在 `IOrganizationService` 中添加一个新方法 `GetVisibleUnitIdsAsync`,根据用户的组织层级返回可见的单位ID列表。对于营部及以下级别,只返回本单位及其直接下级;对于师团级别,返回本单位及所有下级。 + +## Architecture + +```mermaid +flowchart TD + A[ReportsController] --> B[ReportingService] + B --> C[OrganizationService] + C --> D{User Level Check} + D -->|Battalion/Company| E[GetVisibleUnitIdsAsync - Restricted] + D -->|Division/Regiment| F[GetAllSubordinateIdsAsync - Full] + E --> G[Return: Own Unit + Direct Subordinates] + F --> H[Return: Own Unit + All Subordinates] +``` + +## Components and Interfaces + +### 1. IOrganizationService 扩展 + +新增方法用于获取用户可见的单位ID列表: + +```csharp +/// +/// 获取用户可见的单位ID列表 +/// 营部及以下级别:本单位 + 直接下级 +/// 师团级别:本单位 + 所有下级 +/// +Task> GetVisibleUnitIdsAsync(int unitId, OrganizationalLevel userLevel); +``` + +### 2. IReportingService 修改 + +修改现有方法以支持可见性过滤: + +```csharp +/// +/// 获取汇总数据(应用可见性过滤) +/// +Task> GetAggregatedDataAsync(int unitId, OrganizationalLevel userLevel); + +/// +/// 获取详细上报记录(应用可见性过滤) +/// +Task> GetDetailedReportsAsync(int unitId, OrganizationalLevel userLevel); +``` + +### 3. ReportsController 修改 + +在调用服务方法时传递用户的组织层级: + +```csharp +var unitLevel = GetCurrentUnitLevel(); +var reports = await _reportingService.GetDetailedReportsAsync(unitId.Value, unitLevel.Value); +``` + +## Data Models + +无需新增数据模型,使用现有的: +- `OrganizationalUnit` - 组织单位实体 +- `OrganizationalLevel` - 组织层级枚举 +- `AllocationDistribution` - 配额分配记录 +- `ConsumptionReport` - 消耗上报记录 + +### 可见性规则数据流 + +| 用户层级 | 可见范围 | 示例 | +|---------|---------|------| +| Division (师团) | 本单位 + 所有下级 | 师团A → 团B, 团C, 营D, 营E, 连F... | +| Regiment (团) | 本单位 + 所有下级 | 团B → 营D, 营E, 连F, 连G... | +| Battalion (营) | 本单位 + 直接下级 | 营D → 连F, 连G (不含同级营E及其下级) | +| Company (连) | 仅本单位 | 连F → 仅连F | + + + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Restricted visibility for battalion/company level users + +*For any* organization hierarchy and any user at Battalion_Level or Company_Level, the visible unit IDs returned by `GetVisibleUnitIdsAsync` SHALL: +- Include the user's own unit ID +- Include only direct subordinate unit IDs (children) +- Exclude all sibling unit IDs (units with the same parent) +- Exclude all subordinates of sibling units + +**Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** + +### Property 2: Full visibility for division/regiment level users + +*For any* organization hierarchy and any user at Division_Level or Regiment_Level, the visible unit IDs returned by `GetVisibleUnitIdsAsync` SHALL equal the union of the user's own unit ID and all subordinate unit IDs (as returned by `GetAllSubordinateIdsAsync`). + +**Validates: Requirements 1.4, 3.3** + +### Property 3: Access control respects visibility scope + +*For any* user and any target unit ID, the `CanViewUnitDataAsync` method SHALL return `true` if and only if the target unit ID is contained in the user's visible unit IDs set. + +**Validates: Requirements 2.3** + +## Error Handling + +| Scenario | Handling | +|----------|----------| +| User unit not found | Return empty visible units list | +| Invalid organizational level | Default to restricted visibility (fail-safe) | +| Database query failure | Propagate exception to controller for 500 response | + +## Testing Strategy + +### Property-Based Testing + +使用 FsCheck 库进行属性测试(项目已使用 xUnit + FsCheck)。 + +**测试配置:** +- 每个属性测试运行最少 100 次迭代 +- 使用自定义生成器创建有效的组织层级树 + +**属性测试标注格式:** +```csharp +// **Feature: consumption-report-visibility-filter, Property 1: Restricted visibility for battalion/company level users** +// **Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** +``` + +### Unit Tests + +- 测试边界情况:无下级单位的营级用户 +- 测试边界情况:连级用户(最底层) +- 测试师团级用户的完整可见性 + +### Integration Tests (Optional) + +- 验证 API 端点正确应用可见性过滤 +- 验证聚合数据计算使用过滤后的单位列表 diff --git a/.kiro/specs/consumption-report-visibility-filter/requirements.md b/.kiro/specs/consumption-report-visibility-filter/requirements.md new file mode 100644 index 0000000..66df1f4 --- /dev/null +++ b/.kiro/specs/consumption-report-visibility-filter/requirements.md @@ -0,0 +1,47 @@ +# Requirements Document + +## Introduction + +本功能用于限制营部及以下级别账号在上报消耗页面和上报历史中的数据可见性。当前系统允许用户查看所有下级单位的上报记录,但营部及以下级别用户不应看到同级单位及同级单位下级的上报记录,只能查看本单位及本单位直接下级的记录。 + +## Glossary + +- **Consumption_Report_System**: 消耗上报管理系统,负责处理物资消耗的上报、查询和历史记录展示 +- **Battalion_Level**: 营级组织层级(OrganizationalLevel.Battalion = 3) +- **Company_Level**: 连级组织层级(OrganizationalLevel.Company = 4) +- **Sibling_Unit**: 同级单位,指与当前用户所属单位具有相同父级单位的其他组织单位 +- **Subordinate_Unit**: 下级单位,指当前用户所属单位的直接或间接子单位 +- **Report_History**: 上报历史,包含已提交的消耗上报记录列表 + +## Requirements + +### Requirement 1 + +**User Story:** As a battalion-level or company-level user, I want to only see consumption reports from my own unit and my direct subordinates, so that I cannot access data from sibling units or their subordinates. + +#### Acceptance Criteria + +1. WHEN a user with Battalion_Level or Company_Level accesses the report history THEN the Consumption_Report_System SHALL return only records belonging to the user's own unit and the user's direct subordinate units +2. WHEN a user with Battalion_Level or Company_Level queries consumption reports THEN the Consumption_Report_System SHALL exclude all records from sibling units +3. WHEN a user with Battalion_Level or Company_Level queries consumption reports THEN the Consumption_Report_System SHALL exclude all records from subordinates of sibling units +4. WHILE a user is at Division_Level or Regiment_Level THEN the Consumption_Report_System SHALL continue to display all subordinate unit records without filtering + +### Requirement 2 + +**User Story:** As a system administrator, I want the visibility filtering to be applied consistently across all report-related API endpoints, so that data access control is enforced uniformly. + +#### Acceptance Criteria + +1. WHEN the GetDetailedReports endpoint is called by a Battalion_Level or Company_Level user THEN the Consumption_Report_System SHALL apply the visibility filter to exclude sibling unit records +2. WHEN the GetAggregatedData endpoint is called by a Battalion_Level or Company_Level user THEN the Consumption_Report_System SHALL apply the visibility filter to aggregation calculations +3. WHEN the GetByUnit endpoint is called by a Battalion_Level or Company_Level user THEN the Consumption_Report_System SHALL verify the target unit is within the user's visibility scope before returning data + +### Requirement 3 + +**User Story:** As a developer, I want a reusable visibility filter function, so that the filtering logic can be consistently applied and easily tested. + +#### Acceptance Criteria + +1. WHEN determining visible units for a user THEN the Consumption_Report_System SHALL provide a method that returns only the user's own unit ID and all subordinate unit IDs +2. WHEN the user's organizational level is Battalion_Level or Company_Level THEN the Consumption_Report_System SHALL use the restricted visibility scope +3. WHEN the user's organizational level is Division_Level or Regiment_Level THEN the Consumption_Report_System SHALL use the full subordinate visibility scope diff --git a/.kiro/specs/consumption-report-visibility-filter/tasks.md b/.kiro/specs/consumption-report-visibility-filter/tasks.md new file mode 100644 index 0000000..3f454fc --- /dev/null +++ b/.kiro/specs/consumption-report-visibility-filter/tasks.md @@ -0,0 +1,87 @@ +# Implementation Plan + +- [x] 1. Implement visibility filter in OrganizationService + + + + + + - [x] 1.1 Add GetVisibleUnitIdsAsync method to IOrganizationService interface + + + - Add method signature with unitId and userLevel parameters + - Return IEnumerable of visible unit IDs + - _Requirements: 3.1_ + - [x] 1.2 Implement GetVisibleUnitIdsAsync in OrganizationService + + + - For Battalion/Company level: return own unit + direct children only + - For Division/Regiment level: return own unit + all subordinates + - _Requirements: 1.1, 1.2, 1.3, 1.4, 3.2, 3.3_ + - [x] 1.3 Write property test for restricted visibility (Property 1) + + + - **Property 1: Restricted visibility for battalion/company level users** + - **Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** + - [x] 1.4 Write property test for full visibility (Property 2) + + + - **Property 2: Full visibility for division/regiment level users** + - **Validates: Requirements 1.4, 3.3** + +- [x] 2. Update ReportingService to use visibility filter + + + + + + - [x] 2.1 Modify GetAggregatedDataAsync to accept userLevel parameter + + + - Use GetVisibleUnitIdsAsync instead of GetAllSubordinateIdsAsync for filtering + - _Requirements: 2.2_ + - [x] 2.2 Modify GetDetailedReportsAsync to accept userLevel parameter + + + - Apply visibility filter to returned records + - _Requirements: 2.1_ + - [x] 2.3 Update CanViewUnitDataAsync to use visibility scope + + + - Check if target unit is in user's visible units + - _Requirements: 2.3_ + + - [x] 2.4 Write property test for access control (Property 3) + + - **Property 3: Access control respects visibility scope** + - **Validates: Requirements 2.3** + +- [x] 3. Update ReportsController to pass user level + + + + + + - [x] 3.1 Update GetDetailedReports endpoint + + + - Pass unitLevel to service method + - _Requirements: 2.1_ + - [x] 3.2 Update GetAggregatedData endpoint + + + - Pass unitLevel to service method + - _Requirements: 2.2_ + + - [x] 3.3 Update GetByTargetUnit endpoint + + - Use updated CanViewUnitDataAsync with visibility check + - _Requirements: 2.3_ + +- [x] 4. Checkpoint - Ensure all tests pass + + + + + + - Ensure all tests pass, ask the user if questions arise. diff --git a/src/MilitaryTrainingManagement.Tests/Properties/RequiredFieldValidationPropertyTests.cs b/src/MilitaryTrainingManagement.Tests/Properties/RequiredFieldValidationPropertyTests.cs index 13f468f..abe0b7a 100644 --- a/src/MilitaryTrainingManagement.Tests/Properties/RequiredFieldValidationPropertyTests.cs +++ b/src/MilitaryTrainingManagement.Tests/Properties/RequiredFieldValidationPropertyTests.cs @@ -22,11 +22,8 @@ public class RequiredFieldValidationPropertyTests public bool Personnel_WithAllRequiredFields_ValidationPasses( NonEmptyString name, NonEmptyString position, - NonEmptyString rank, - PositiveInt ageInt) + NonEmptyString rank) { - var age = Math.Min(60, Math.Max(18, ageInt.Get % 43 + 18)); // 18-60岁 - // 确保生成的字符串是有效的(非空白字符) var validName = SanitizeString(name.Get, 50, 2); var validPosition = SanitizeString(position.Get, 100, 1); @@ -37,9 +34,7 @@ public class RequiredFieldValidationPropertyTests Name = validName, Position = validPosition, Rank = validRank, - Gender = "男", - IdNumber = GenerateValidIdNumber(), - Age = age + IdNumber = GenerateValidIdNumber() }; var validationResults = ValidateModel(request); @@ -53,19 +48,14 @@ public class RequiredFieldValidationPropertyTests [Property(MaxTest = 100)] public bool Personnel_WithoutName_ValidationFails( NonEmptyString position, - NonEmptyString rank, - PositiveInt ageInt) + NonEmptyString rank) { - var age = Math.Min(60, Math.Max(18, ageInt.Get % 43 + 18)); - var request = new CreatePersonnelRequest { Name = "", // 缺少姓名 Position = TruncateString(position.Get, 100), Rank = TruncateString(rank.Get, 50), - Gender = "男", - IdNumber = GenerateValidIdNumber(), - Age = age + IdNumber = GenerateValidIdNumber() }; var validationResults = ValidateModel(request); @@ -80,19 +70,14 @@ public class RequiredFieldValidationPropertyTests [Property(MaxTest = 100)] public bool Personnel_WithoutPosition_ValidationFails( NonEmptyString name, - NonEmptyString rank, - PositiveInt ageInt) + NonEmptyString rank) { - var age = Math.Min(60, Math.Max(18, ageInt.Get % 43 + 18)); - var request = new CreatePersonnelRequest { Name = TruncateString(name.Get, 50), Position = "", // 缺少职位 Rank = TruncateString(rank.Get, 50), - Gender = "男", - IdNumber = GenerateValidIdNumber(), - Age = age + IdNumber = GenerateValidIdNumber() }; var validationResults = ValidateModel(request); @@ -106,60 +91,20 @@ public class RequiredFieldValidationPropertyTests [Property(MaxTest = 100)] public bool Personnel_WithoutRank_ValidationFails( NonEmptyString name, - NonEmptyString position, - PositiveInt ageInt) + NonEmptyString position) { - var age = Math.Min(60, Math.Max(18, ageInt.Get % 43 + 18)); - var request = new CreatePersonnelRequest { Name = TruncateString(name.Get, 50), Position = TruncateString(position.Get, 100), Rank = "", // 缺少军衔 - Gender = "男", - IdNumber = GenerateValidIdNumber(), - Age = age + IdNumber = GenerateValidIdNumber() }; var validationResults = ValidateModel(request); return validationResults.Any(r => r.MemberNames.Contains("Rank")); } - /// - /// 属性7扩展:无效性别的人员数据应该验证失败 - /// **验证需求:需求6.1** - /// - [Property(MaxTest = 100)] - public bool Personnel_WithInvalidGender_ValidationFails( - NonEmptyString name, - NonEmptyString position, - NonEmptyString rank, - NonEmptyString invalidGender, - PositiveInt ageInt) - { - // 确保生成的性别不是有效值 - var gender = invalidGender.Get; - if (gender == "男" || gender == "女") - { - gender = "无效"; - } - - var age = Math.Min(60, Math.Max(18, ageInt.Get % 43 + 18)); - - var request = new CreatePersonnelRequest - { - Name = TruncateString(name.Get, 50), - Position = TruncateString(position.Get, 100), - Rank = TruncateString(rank.Get, 50), - Gender = gender, // 无效性别 - IdNumber = GenerateValidIdNumber(), - Age = age - }; - - var validationResults = ValidateModel(request); - return validationResults.Any(r => r.MemberNames.Contains("Gender")); - } - /// /// 属性7扩展:无效身份证号的人员数据应该验证失败 /// **验证需求:需求6.1** @@ -168,55 +113,20 @@ public class RequiredFieldValidationPropertyTests public bool Personnel_WithInvalidIdNumber_ValidationFails( NonEmptyString name, NonEmptyString position, - NonEmptyString rank, - PositiveInt ageInt) + NonEmptyString rank) { - var age = Math.Min(60, Math.Max(18, ageInt.Get % 43 + 18)); - var request = new CreatePersonnelRequest { Name = TruncateString(name.Get, 50), Position = TruncateString(position.Get, 100), Rank = TruncateString(rank.Get, 50), - Gender = "男", - IdNumber = "invalid123", // 无效身份证号 - Age = age + IdNumber = "invalid123" // 无效身份证号 }; var validationResults = ValidateModel(request); return validationResults.Any(r => r.MemberNames.Contains("IdNumber")); } - /// - /// 属性7扩展:年龄超出范围的人员数据应该验证失败 - /// **验证需求:需求6.1** - /// - [Property(MaxTest = 100)] - public bool Personnel_WithInvalidAge_ValidationFails( - NonEmptyString name, - NonEmptyString position, - NonEmptyString rank, - PositiveInt invalidAgeInt) - { - // 生成超出范围的年龄(小于18或大于60) - var invalidAge = invalidAgeInt.Get % 2 == 0 - ? invalidAgeInt.Get % 17 + 1 // 1-17岁 - : invalidAgeInt.Get % 40 + 61; // 61-100岁 - - var request = new CreatePersonnelRequest - { - Name = TruncateString(name.Get, 50), - Position = TruncateString(position.Get, 100), - Rank = TruncateString(rank.Get, 50), - Gender = "男", - IdNumber = GenerateValidIdNumber(), - Age = invalidAge - }; - - var validationResults = ValidateModel(request); - return validationResults.Any(r => r.MemberNames.Contains("Age")); - } - /// /// 属性7扩展:物资配额必填字段验证 diff --git a/src/MilitaryTrainingManagement.Tests/Properties/VisibilityFilterPropertyTests.cs b/src/MilitaryTrainingManagement.Tests/Properties/VisibilityFilterPropertyTests.cs new file mode 100644 index 0000000..5d767a4 --- /dev/null +++ b/src/MilitaryTrainingManagement.Tests/Properties/VisibilityFilterPropertyTests.cs @@ -0,0 +1,878 @@ +using FsCheck; +using FsCheck.Xunit; +using Microsoft.EntityFrameworkCore; +using MilitaryTrainingManagement.Data; +using MilitaryTrainingManagement.Models.Entities; +using MilitaryTrainingManagement.Models.Enums; +using MilitaryTrainingManagement.Services.Implementations; + +namespace MilitaryTrainingManagement.Tests.Properties; + +/// +/// 消耗上报可见性过滤属性测试 +/// **Feature: consumption-report-visibility-filter** +/// +public class VisibilityFilterPropertyTests +{ + /// + /// 创建内存数据库上下文 + /// + private static ApplicationDbContext CreateInMemoryContext(string dbName) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: dbName) + .Options; + return new ApplicationDbContext(options); + } + + /// + /// 创建测试用的组织层级结构(包含同级单位) + /// 结构: + /// - Division (师团) + /// - Regiment (团) + /// - Battalion1 (营1) - 测试用户所在单位 + /// - Company1 (连1) + /// - Company2 (连2) + /// - Battalion2 (营2) - 同级单位 + /// - Company3 (连3) + /// - Company4 (连4) + /// + private static TestHierarchy CreateHierarchyWithSiblings(ApplicationDbContext context) + { + var division = new OrganizationalUnit + { + Name = "师团", + Level = OrganizationalLevel.Division, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(division); + context.SaveChanges(); + + var regiment = new OrganizationalUnit + { + Name = "团", + Level = OrganizationalLevel.Regiment, + ParentId = division.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(regiment); + context.SaveChanges(); + + // 营1 - 测试用户所在单位 + var battalion1 = new OrganizationalUnit + { + Name = "营1", + Level = OrganizationalLevel.Battalion, + ParentId = regiment.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(battalion1); + context.SaveChanges(); + + // 营2 - 同级单位 + var battalion2 = new OrganizationalUnit + { + Name = "营2", + Level = OrganizationalLevel.Battalion, + ParentId = regiment.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(battalion2); + context.SaveChanges(); + + // 连1, 连2 - 营1的下级 + var company1 = new OrganizationalUnit + { + Name = "连1", + Level = OrganizationalLevel.Company, + ParentId = battalion1.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company1); + + var company2 = new OrganizationalUnit + { + Name = "连2", + Level = OrganizationalLevel.Company, + ParentId = battalion1.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company2); + context.SaveChanges(); + + // 连3, 连4 - 营2的下级(同级单位的下级) + var company3 = new OrganizationalUnit + { + Name = "连3", + Level = OrganizationalLevel.Company, + ParentId = battalion2.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company3); + + var company4 = new OrganizationalUnit + { + Name = "连4", + Level = OrganizationalLevel.Company, + ParentId = battalion2.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company4); + context.SaveChanges(); + + return new TestHierarchy + { + Division = division, + Regiment = regiment, + Battalion1 = battalion1, + Battalion2 = battalion2, + Company1 = company1, + Company2 = company2, + Company3 = company3, + Company4 = company4 + }; + } + + private class TestHierarchy + { + public OrganizationalUnit Division { get; set; } = null!; + public OrganizationalUnit Regiment { get; set; } = null!; + public OrganizationalUnit Battalion1 { get; set; } = null!; + public OrganizationalUnit Battalion2 { get; set; } = null!; + public OrganizationalUnit Company1 { get; set; } = null!; + public OrganizationalUnit Company2 { get; set; } = null!; + public OrganizationalUnit Company3 { get; set; } = null!; + public OrganizationalUnit Company4 { get; set; } = null!; + } + + + /// + /// **Feature: consumption-report-visibility-filter, Property 1: Restricted visibility for battalion/company level users** + /// **Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** + /// + /// *For any* organization hierarchy and any user at Battalion_Level or Company_Level, + /// the visible unit IDs returned by GetVisibleUnitIdsAsync SHALL: + /// - Include the user's own unit ID + /// - Include only direct subordinate unit IDs (children) + /// - Exclude all sibling unit IDs (units with the same parent) + /// - Exclude all subordinates of sibling units + /// + [Property(MaxTest = 100)] + public bool RestrictedVisibility_BattalionCompanyLevel_OnlyShowsOwnUnitAndDirectChildren() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test for Battalion level user (营1) + var battalionVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Battalion1.Id, OrganizationalLevel.Battalion) + .GetAwaiter().GetResult() + .ToList(); + + // 营级用户应该能看到:本单位 + 直接下级(连1, 连2) + var expectedBattalionVisible = new HashSet + { + hierarchy.Battalion1.Id, + hierarchy.Company1.Id, + hierarchy.Company2.Id + }; + + // 验证包含所有预期的单位 + var containsAllExpected = expectedBattalionVisible.All(id => battalionVisibleUnits.Contains(id)); + + // 验证不包含同级单位(营2) + var excludesSibling = !battalionVisibleUnits.Contains(hierarchy.Battalion2.Id); + + // 验证不包含同级单位的下级(连3, 连4) + var excludesSiblingSubordinates = !battalionVisibleUnits.Contains(hierarchy.Company3.Id) + && !battalionVisibleUnits.Contains(hierarchy.Company4.Id); + + // 验证数量正确(只有3个:营1, 连1, 连2) + var correctCount = battalionVisibleUnits.Count == 3; + + return containsAllExpected && excludesSibling && excludesSiblingSubordinates && correctCount; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 1: Restricted visibility for battalion/company level users** + /// **Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** + /// + /// 连级用户只能看到本单位 + /// + [Property(MaxTest = 100)] + public bool RestrictedVisibility_CompanyLevel_OnlyShowsOwnUnit() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test for Company level user (连1) + var companyVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Company1.Id, OrganizationalLevel.Company) + .GetAwaiter().GetResult() + .ToList(); + + // 连级用户应该只能看到本单位(连级没有下级) + var containsOwnUnit = companyVisibleUnits.Contains(hierarchy.Company1.Id); + + // 验证不包含同级单位(连2) + var excludesSibling = !companyVisibleUnits.Contains(hierarchy.Company2.Id); + + // 验证不包含上级单位 + var excludesParent = !companyVisibleUnits.Contains(hierarchy.Battalion1.Id); + + // 验证数量正确(只有1个:连1) + var correctCount = companyVisibleUnits.Count == 1; + + return containsOwnUnit && excludesSibling && excludesParent && correctCount; + } + + + /// + /// **Feature: consumption-report-visibility-filter, Property 1: Restricted visibility for battalion/company level users** + /// **Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** + /// + /// *For any* user at Battalion or Company level, visible units must include own unit + /// + [Property(MaxTest = 100)] + public bool RestrictedVisibility_AlwaysIncludesOwnUnit(PositiveInt seed) + { + var dbName = $"test_{seed.Get}_{Guid.NewGuid()}"; + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test both Battalion and Company levels + var restrictedLevels = new[] { OrganizationalLevel.Battalion, OrganizationalLevel.Company }; + var testUnits = new[] { hierarchy.Battalion1, hierarchy.Company1 }; + + for (int i = 0; i < restrictedLevels.Length; i++) + { + var visibleUnits = organizationService + .GetVisibleUnitIdsAsync(testUnits[i].Id, restrictedLevels[i]) + .GetAwaiter().GetResult() + .ToList(); + + // 必须包含自己的单位ID + if (!visibleUnits.Contains(testUnits[i].Id)) + return false; + } + + return true; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 1: Restricted visibility for battalion/company level users** + /// **Validates: Requirements 1.1, 1.2, 1.3, 3.1, 3.2** + /// + /// *For any* user at Battalion or Company level, visible units must exclude sibling units + /// + [Property(MaxTest = 100)] + public bool RestrictedVisibility_ExcludesSiblingUnits(PositiveInt seed) + { + var dbName = $"test_{seed.Get}_{Guid.NewGuid()}"; + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // 营1用户不应该看到营2(同级单位) + var battalionVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Battalion1.Id, OrganizationalLevel.Battalion) + .GetAwaiter().GetResult() + .ToList(); + + // 不应包含同级单位 + if (battalionVisibleUnits.Contains(hierarchy.Battalion2.Id)) + return false; + + // 不应包含同级单位的下级 + if (battalionVisibleUnits.Contains(hierarchy.Company3.Id) || + battalionVisibleUnits.Contains(hierarchy.Company4.Id)) + return false; + + return true; + } +} + + +/// +/// 师团/团级用户完整可见性属性测试 +/// **Feature: consumption-report-visibility-filter, Property 2: Full visibility for division/regiment level users** +/// +public class FullVisibilityPropertyTests +{ + /// + /// 创建内存数据库上下文 + /// + private static ApplicationDbContext CreateInMemoryContext(string dbName) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: dbName) + .Options; + return new ApplicationDbContext(options); + } + + /// + /// 创建测试用的组织层级结构(包含同级单位) + /// + private static TestHierarchy CreateHierarchyWithSiblings(ApplicationDbContext context) + { + var division = new OrganizationalUnit + { + Name = "师团", + Level = OrganizationalLevel.Division, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(division); + context.SaveChanges(); + + var regiment = new OrganizationalUnit + { + Name = "团", + Level = OrganizationalLevel.Regiment, + ParentId = division.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(regiment); + context.SaveChanges(); + + // 营1 + var battalion1 = new OrganizationalUnit + { + Name = "营1", + Level = OrganizationalLevel.Battalion, + ParentId = regiment.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(battalion1); + context.SaveChanges(); + + // 营2 - 同级单位 + var battalion2 = new OrganizationalUnit + { + Name = "营2", + Level = OrganizationalLevel.Battalion, + ParentId = regiment.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(battalion2); + context.SaveChanges(); + + // 连1, 连2 - 营1的下级 + var company1 = new OrganizationalUnit + { + Name = "连1", + Level = OrganizationalLevel.Company, + ParentId = battalion1.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company1); + + var company2 = new OrganizationalUnit + { + Name = "连2", + Level = OrganizationalLevel.Company, + ParentId = battalion1.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company2); + context.SaveChanges(); + + // 连3, 连4 - 营2的下级 + var company3 = new OrganizationalUnit + { + Name = "连3", + Level = OrganizationalLevel.Company, + ParentId = battalion2.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company3); + + var company4 = new OrganizationalUnit + { + Name = "连4", + Level = OrganizationalLevel.Company, + ParentId = battalion2.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company4); + context.SaveChanges(); + + return new TestHierarchy + { + Division = division, + Regiment = regiment, + Battalion1 = battalion1, + Battalion2 = battalion2, + Company1 = company1, + Company2 = company2, + Company3 = company3, + Company4 = company4 + }; + } + + private class TestHierarchy + { + public OrganizationalUnit Division { get; set; } = null!; + public OrganizationalUnit Regiment { get; set; } = null!; + public OrganizationalUnit Battalion1 { get; set; } = null!; + public OrganizationalUnit Battalion2 { get; set; } = null!; + public OrganizationalUnit Company1 { get; set; } = null!; + public OrganizationalUnit Company2 { get; set; } = null!; + public OrganizationalUnit Company3 { get; set; } = null!; + public OrganizationalUnit Company4 { get; set; } = null!; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 2: Full visibility for division/regiment level users** + /// **Validates: Requirements 1.4, 3.3** + /// + /// *For any* organization hierarchy and any user at Division_Level or Regiment_Level, + /// the visible unit IDs returned by GetVisibleUnitIdsAsync SHALL equal the union of + /// the user's own unit ID and all subordinate unit IDs (as returned by GetAllSubordinateIdsAsync). + /// + [Property(MaxTest = 100)] + public bool FullVisibility_DivisionLevel_ShowsAllSubordinates() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test for Division level user + var divisionVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Division.Id, OrganizationalLevel.Division) + .GetAwaiter().GetResult() + .ToHashSet(); + + // 获取所有下级单位ID + var allSubordinates = organizationService + .GetAllSubordinateIdsAsync(hierarchy.Division.Id) + .GetAwaiter().GetResult() + .ToHashSet(); + + // 预期可见单位 = 本单位 + 所有下级 + var expectedVisible = new HashSet { hierarchy.Division.Id }; + expectedVisible.UnionWith(allSubordinates); + + // 验证可见单位集合与预期完全相等 + return divisionVisibleUnits.SetEquals(expectedVisible); + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 2: Full visibility for division/regiment level users** + /// **Validates: Requirements 1.4, 3.3** + /// + /// 团级用户应该能看到所有下级单位 + /// + [Property(MaxTest = 100)] + public bool FullVisibility_RegimentLevel_ShowsAllSubordinates() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test for Regiment level user + var regimentVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Regiment.Id, OrganizationalLevel.Regiment) + .GetAwaiter().GetResult() + .ToHashSet(); + + // 获取所有下级单位ID + var allSubordinates = organizationService + .GetAllSubordinateIdsAsync(hierarchy.Regiment.Id) + .GetAwaiter().GetResult() + .ToHashSet(); + + // 预期可见单位 = 本单位 + 所有下级 + var expectedVisible = new HashSet { hierarchy.Regiment.Id }; + expectedVisible.UnionWith(allSubordinates); + + // 验证可见单位集合与预期完全相等 + return regimentVisibleUnits.SetEquals(expectedVisible); + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 2: Full visibility for division/regiment level users** + /// **Validates: Requirements 1.4, 3.3** + /// + /// 师团级用户应该能看到所有营和连级单位(包括同级单位的下级) + /// + [Property(MaxTest = 100)] + public bool FullVisibility_DivisionLevel_IncludesAllBattalionsAndCompanies() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test for Division level user + var divisionVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Division.Id, OrganizationalLevel.Division) + .GetAwaiter().GetResult() + .ToList(); + + // 师团级用户应该能看到所有单位 + var allUnits = new[] + { + hierarchy.Division.Id, + hierarchy.Regiment.Id, + hierarchy.Battalion1.Id, + hierarchy.Battalion2.Id, + hierarchy.Company1.Id, + hierarchy.Company2.Id, + hierarchy.Company3.Id, + hierarchy.Company4.Id + }; + + // 验证包含所有单位 + return allUnits.All(id => divisionVisibleUnits.Contains(id)) + && divisionVisibleUnits.Count == allUnits.Length; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 2: Full visibility for division/regiment level users** + /// **Validates: Requirements 1.4, 3.3** + /// + /// 团级用户应该能看到所有营和连级单位 + /// + [Property(MaxTest = 100)] + public bool FullVisibility_RegimentLevel_IncludesAllBattalionsAndCompanies() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + + // Test for Regiment level user + var regimentVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Regiment.Id, OrganizationalLevel.Regiment) + .GetAwaiter().GetResult() + .ToList(); + + // 团级用户应该能看到:团 + 营1 + 营2 + 连1 + 连2 + 连3 + 连4 + var expectedUnits = new[] + { + hierarchy.Regiment.Id, + hierarchy.Battalion1.Id, + hierarchy.Battalion2.Id, + hierarchy.Company1.Id, + hierarchy.Company2.Id, + hierarchy.Company3.Id, + hierarchy.Company4.Id + }; + + // 验证包含所有预期单位 + return expectedUnits.All(id => regimentVisibleUnits.Contains(id)) + && regimentVisibleUnits.Count == expectedUnits.Length; + } +} + + +/// +/// 访问控制属性测试 +/// **Feature: consumption-report-visibility-filter, Property 3: Access control respects visibility scope** +/// +public class AccessControlPropertyTests +{ + /// + /// 创建内存数据库上下文 + /// + private static ApplicationDbContext CreateInMemoryContext(string dbName) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: dbName) + .Options; + return new ApplicationDbContext(options); + } + + /// + /// 创建测试用的组织层级结构(包含同级单位) + /// + private static TestHierarchy CreateHierarchyWithSiblings(ApplicationDbContext context) + { + var division = new OrganizationalUnit + { + Name = "师团", + Level = OrganizationalLevel.Division, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(division); + context.SaveChanges(); + + var regiment = new OrganizationalUnit + { + Name = "团", + Level = OrganizationalLevel.Regiment, + ParentId = division.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(regiment); + context.SaveChanges(); + + // 营1 + var battalion1 = new OrganizationalUnit + { + Name = "营1", + Level = OrganizationalLevel.Battalion, + ParentId = regiment.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(battalion1); + context.SaveChanges(); + + // 营2 - 同级单位 + var battalion2 = new OrganizationalUnit + { + Name = "营2", + Level = OrganizationalLevel.Battalion, + ParentId = regiment.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(battalion2); + context.SaveChanges(); + + // 连1, 连2 - 营1的下级 + var company1 = new OrganizationalUnit + { + Name = "连1", + Level = OrganizationalLevel.Company, + ParentId = battalion1.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company1); + + var company2 = new OrganizationalUnit + { + Name = "连2", + Level = OrganizationalLevel.Company, + ParentId = battalion1.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company2); + context.SaveChanges(); + + // 连3, 连4 - 营2的下级 + var company3 = new OrganizationalUnit + { + Name = "连3", + Level = OrganizationalLevel.Company, + ParentId = battalion2.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company3); + + var company4 = new OrganizationalUnit + { + Name = "连4", + Level = OrganizationalLevel.Company, + ParentId = battalion2.Id, + CreatedAt = DateTime.UtcNow + }; + context.OrganizationalUnits.Add(company4); + context.SaveChanges(); + + return new TestHierarchy + { + Division = division, + Regiment = regiment, + Battalion1 = battalion1, + Battalion2 = battalion2, + Company1 = company1, + Company2 = company2, + Company3 = company3, + Company4 = company4 + }; + } + + private class TestHierarchy + { + public OrganizationalUnit Division { get; set; } = null!; + public OrganizationalUnit Regiment { get; set; } = null!; + public OrganizationalUnit Battalion1 { get; set; } = null!; + public OrganizationalUnit Battalion2 { get; set; } = null!; + public OrganizationalUnit Company1 { get; set; } = null!; + public OrganizationalUnit Company2 { get; set; } = null!; + public OrganizationalUnit Company3 { get; set; } = null!; + public OrganizationalUnit Company4 { get; set; } = null!; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 3: Access control respects visibility scope** + /// **Validates: Requirements 2.3** + /// + /// *For any* user and any target unit ID, the CanViewUnitDataAsync method SHALL return true + /// if and only if the target unit ID is contained in the user's visible unit IDs set. + /// + [Property(MaxTest = 100)] + public bool AccessControl_ReturnsTrue_WhenTargetInVisibleUnits() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + var reportingService = new ReportingService(context, organizationService); + + // 测试营级用户访问可见单位 + var battalionVisibleUnits = organizationService + .GetVisibleUnitIdsAsync(hierarchy.Battalion1.Id, OrganizationalLevel.Battalion) + .GetAwaiter().GetResult() + .ToList(); + + // 对于每个可见单位,CanViewUnitDataAsync 应该返回 true + foreach (var visibleUnitId in battalionVisibleUnits) + { + var canView = reportingService + .CanViewUnitDataAsync(hierarchy.Battalion1.Id, visibleUnitId, OrganizationalLevel.Battalion) + .GetAwaiter().GetResult(); + + if (!canView) + return false; + } + + return true; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 3: Access control respects visibility scope** + /// **Validates: Requirements 2.3** + /// + /// *For any* user and any target unit ID not in visible units, CanViewUnitDataAsync SHALL return false. + /// + [Property(MaxTest = 100)] + public bool AccessControl_ReturnsFalse_WhenTargetNotInVisibleUnits() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + var reportingService = new ReportingService(context, organizationService); + + // 测试营级用户访问不可见单位(同级单位及其下级) + var invisibleUnits = new[] + { + hierarchy.Battalion2.Id, // 同级单位 + hierarchy.Company3.Id, // 同级单位的下级 + hierarchy.Company4.Id, // 同级单位的下级 + hierarchy.Regiment.Id, // 上级单位 + hierarchy.Division.Id // 更上级单位 + }; + + foreach (var invisibleUnitId in invisibleUnits) + { + var canView = reportingService + .CanViewUnitDataAsync(hierarchy.Battalion1.Id, invisibleUnitId, OrganizationalLevel.Battalion) + .GetAwaiter().GetResult(); + + if (canView) + return false; + } + + return true; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 3: Access control respects visibility scope** + /// **Validates: Requirements 2.3** + /// + /// *For any* user at Division/Regiment level, CanViewUnitDataAsync SHALL return true for all subordinates. + /// + [Property(MaxTest = 100)] + public bool AccessControl_DivisionLevel_CanViewAllSubordinates() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + var reportingService = new ReportingService(context, organizationService); + + // 师团级用户应该能访问所有下级单位 + var allUnits = new[] + { + hierarchy.Division.Id, + hierarchy.Regiment.Id, + hierarchy.Battalion1.Id, + hierarchy.Battalion2.Id, + hierarchy.Company1.Id, + hierarchy.Company2.Id, + hierarchy.Company3.Id, + hierarchy.Company4.Id + }; + + foreach (var unitId in allUnits) + { + var canView = reportingService + .CanViewUnitDataAsync(hierarchy.Division.Id, unitId, OrganizationalLevel.Division) + .GetAwaiter().GetResult(); + + if (!canView) + return false; + } + + return true; + } + + /// + /// **Feature: consumption-report-visibility-filter, Property 3: Access control respects visibility scope** + /// **Validates: Requirements 2.3** + /// + /// CanViewUnitDataAsync result is consistent with GetVisibleUnitIdsAsync for all user levels. + /// + [Property(MaxTest = 100)] + public bool AccessControl_ConsistentWithVisibleUnits_AllLevels() + { + var dbName = Guid.NewGuid().ToString(); + using var context = CreateInMemoryContext(dbName); + var hierarchy = CreateHierarchyWithSiblings(context); + var organizationService = new OrganizationService(context); + var reportingService = new ReportingService(context, organizationService); + + // 测试所有层级 + var testCases = new[] + { + (hierarchy.Division.Id, OrganizationalLevel.Division), + (hierarchy.Regiment.Id, OrganizationalLevel.Regiment), + (hierarchy.Battalion1.Id, OrganizationalLevel.Battalion), + (hierarchy.Company1.Id, OrganizationalLevel.Company) + }; + + var allUnits = new[] + { + hierarchy.Division.Id, + hierarchy.Regiment.Id, + hierarchy.Battalion1.Id, + hierarchy.Battalion2.Id, + hierarchy.Company1.Id, + hierarchy.Company2.Id, + hierarchy.Company3.Id, + hierarchy.Company4.Id + }; + + foreach (var (userUnitId, userLevel) in testCases) + { + var visibleUnits = organizationService + .GetVisibleUnitIdsAsync(userUnitId, userLevel) + .GetAwaiter().GetResult() + .ToHashSet(); + + foreach (var targetUnitId in allUnits) + { + var canView = reportingService + .CanViewUnitDataAsync(userUnitId, targetUnitId, userLevel) + .GetAwaiter().GetResult(); + + var shouldBeVisible = visibleUnits.Contains(targetUnitId); + + // CanViewUnitDataAsync 的结果应该与 GetVisibleUnitIdsAsync 一致 + if (canView != shouldBeVisible) + return false; + } + } + + return true; + } +} diff --git a/src/MilitaryTrainingManagement.Tests/Services/ReportingServiceTests.cs b/src/MilitaryTrainingManagement.Tests/Services/ReportingServiceTests.cs index b6fdb9d..6895272 100644 --- a/src/MilitaryTrainingManagement.Tests/Services/ReportingServiceTests.cs +++ b/src/MilitaryTrainingManagement.Tests/Services/ReportingServiceTests.cs @@ -275,8 +275,8 @@ public class ReportingServiceTests CreateDistribution(context, hierarchy[OrganizationalLevel.Division], hierarchy[OrganizationalLevel.Battalion], 50m); - // Act - var result = await service.GetAggregatedDataAsync(hierarchy[OrganizationalLevel.Division].Id); + // Act - 使用师团级别,应该能看到所有下级 + var result = await service.GetAggregatedDataAsync(hierarchy[OrganizationalLevel.Division].Id, OrganizationalLevel.Division); // Assert Assert.Equal(2, result.Count()); @@ -352,7 +352,7 @@ public class ReportingServiceTests var unitId = hierarchy[OrganizationalLevel.Regiment].Id; // Act - var canView = await service.CanViewUnitDataAsync(unitId, unitId); + var canView = await service.CanViewUnitDataAsync(unitId, unitId, OrganizationalLevel.Regiment); // Assert Assert.True(canView); @@ -374,8 +374,8 @@ public class ReportingServiceTests var divisionId = hierarchy[OrganizationalLevel.Division].Id; var regimentId = hierarchy[OrganizationalLevel.Regiment].Id; - // Act - var canView = await service.CanViewUnitDataAsync(divisionId, regimentId); + // Act - 师团级别可以查看所有下级 + var canView = await service.CanViewUnitDataAsync(divisionId, regimentId, OrganizationalLevel.Division); // Assert Assert.True(canView); @@ -397,8 +397,8 @@ public class ReportingServiceTests var divisionId = hierarchy[OrganizationalLevel.Division].Id; var regimentId = hierarchy[OrganizationalLevel.Regiment].Id; - // Act - var canView = await service.CanViewUnitDataAsync(regimentId, divisionId); + // Act - 团级不能查看师团级 + var canView = await service.CanViewUnitDataAsync(regimentId, divisionId, OrganizationalLevel.Regiment); // Assert Assert.False(canView); @@ -420,8 +420,8 @@ public class ReportingServiceTests var divisionId = hierarchy[OrganizationalLevel.Division].Id; var companyId = hierarchy[OrganizationalLevel.Company].Id; - // Act - var canView = await service.CanViewUnitDataAsync(divisionId, companyId); + // Act - 师团级别可以查看所有下级(包括连级) + var canView = await service.CanViewUnitDataAsync(divisionId, companyId, OrganizationalLevel.Division); // Assert Assert.True(canView); @@ -448,8 +448,8 @@ public class ReportingServiceTests hierarchy[OrganizationalLevel.Regiment], 100m); await service.SubmitReportAsync(distribution.Id, 80m, user.Id); - // Act - var reports = await service.GetDetailedReportsAsync(hierarchy[OrganizationalLevel.Division].Id); + // Act - 使用师团级别,应该能看到所有下级 + var reports = await service.GetDetailedReportsAsync(hierarchy[OrganizationalLevel.Division].Id, OrganizationalLevel.Division); // Assert var report = reports.First(); @@ -475,8 +475,8 @@ public class ReportingServiceTests hierarchy[OrganizationalLevel.Regiment], 100m); await service.SubmitReportAsync(distribution.Id, 80m, user.Id); - // Act - var reports = await service.GetDetailedReportsAsync(hierarchy[OrganizationalLevel.Division].Id); + // Act - 使用师团级别,应该能看到所有下级 + var reports = await service.GetDetailedReportsAsync(hierarchy[OrganizationalLevel.Division].Id, OrganizationalLevel.Division); // Assert var report = reports.First(); diff --git a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs index bb14426..f9ed8c2 100644 --- a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs @@ -98,29 +98,78 @@ public class AllocationsController : BaseApiController /// /// 根据ID获取物资配额 + /// 需求1.1, 1.2, 1.3:营部及以下级别用户只能查看本单位及直接下级的配额 /// [HttpGet("{id}")] public async Task GetById(int id) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) return Unauthorized(new { message = "无法获取用户组织信息" }); var allocation = await _allocationService.GetByIdAsync(id); if (allocation == null) return NotFound(new { message = "配额不存在" }); - // 检查访问权限:可以查看自己创建的、分配给自己及下级的、或分配给上级单位的配额 - var canAccess = allocation.CreatedByUnitId == unitId.Value || - allocation.Distributions.Any(d => - _authorizationService.CanAccessUnitAsync(unitId.Value, d.TargetUnitId).GetAwaiter().GetResult()) || - allocation.Distributions.Any(d => - _authorizationService.IsAncestorUnitAsync(d.TargetUnitId, unitId.Value).GetAwaiter().GetResult()); + // 检查访问权限: + // 1. 可以查看自己创建的配额 + // 2. 使用可见性过滤检查是否可以访问配额中的任何一个分配记录 + var canAccess = allocation.CreatedByUnitId == unitId.Value; + + if (!canAccess) + { + // 检查是否有任何分配记录在用户的可见范围内 + foreach (var dist in allocation.Distributions) + { + if (await _allocationService.CanViewDistributionAsync(unitId.Value, unitLevel.Value, dist.TargetUnitId)) + { + canAccess = true; + break; + } + } + } if (!canAccess) return Forbid(); - return Ok(MapToResponse(allocation)); + // 过滤分配记录,只返回用户可见范围内的记录 + var filteredAllocation = await FilterAllocationDistributions(allocation, unitId.Value, unitLevel.Value); + + return Ok(MapToResponse(filteredAllocation)); + } + + /// + /// 过滤配额的分配记录,只保留用户可见范围内的记录 + /// + private async Task FilterAllocationDistributions( + Models.Entities.MaterialAllocation allocation, + int userUnitId, + OrganizationalLevel userLevel) + { + var visibleDistributions = new List(); + + foreach (var dist in allocation.Distributions) + { + if (await _allocationService.CanViewDistributionAsync(userUnitId, userLevel, dist.TargetUnitId)) + { + visibleDistributions.Add(dist); + } + } + + // 创建一个新的配额对象,只包含可见的分配记录 + return new Models.Entities.MaterialAllocation + { + Id = allocation.Id, + Category = allocation.Category, + MaterialName = allocation.MaterialName, + Unit = allocation.Unit, + TotalQuota = allocation.TotalQuota, + CreatedByUnitId = allocation.CreatedByUnitId, + CreatedByUnit = allocation.CreatedByUnit, + CreatedAt = allocation.CreatedAt, + Distributions = visibleDistributions + }; } /// @@ -322,6 +371,7 @@ public class AllocationsController : BaseApiController /// /// 获取配额分配的上报历史记录 + /// 需求1.1, 1.2, 1.3:营部及以下级别用户只能查看本单位及直接下级的记录 /// [HttpGet("distributions/{distributionId}/reports")] public async Task GetConsumptionReports(int distributionId) @@ -329,7 +379,7 @@ public class AllocationsController : BaseApiController var unitId = GetCurrentUnitId(); var unitLevel = GetCurrentUnitLevel(); - if (unitId == null) + if (unitId == null || unitLevel == null) return Unauthorized(new { message = "无法获取用户组织信息" }); // 检查分配记录是否存在 @@ -337,20 +387,20 @@ public class AllocationsController : BaseApiController if (distribution == null) return NotFound(new { message = "配额分配记录不存在" }); - // 检查访问权限: - // 1. 师团级可以查看所有记录 - // 2. 可以查看分配给自己单位的记录 - // 3. 可以查看分配给上级单位的记录 - // 4. 可以查看分配给下级单位的记录 - var canAccess = unitLevel == OrganizationalLevel.Division || - distribution.TargetUnitId == unitId.Value || - await _authorizationService.IsAncestorUnitAsync(distribution.TargetUnitId, unitId.Value) || - await _authorizationService.CanAccessUnitAsync(unitId.Value, distribution.TargetUnitId); + // 使用可见性过滤检查访问权限 + // 营部及以下级别:只能查看本单位及直接下级的记录 + // 师团/团级:可以查看所有下级的记录 + var canAccess = await _allocationService.CanViewDistributionAsync( + unitId.Value, + unitLevel.Value, + distribution.TargetUnitId); if (!canAccess) return Forbid(); - var reports = await _allocationService.GetConsumptionReportsAsync(distributionId); + // 获取上报历史记录(带可见性过滤) + // 只返回用户可见范围内的单位的上报记录 + var reports = await _allocationService.GetConsumptionReportsAsync(distributionId, unitId.Value, unitLevel.Value); var response = reports.Select(r => new { id = r.Id, diff --git a/src/MilitaryTrainingManagement/Controllers/ReportsController.cs b/src/MilitaryTrainingManagement/Controllers/ReportsController.cs index 0394dc5..5ae6a45 100644 --- a/src/MilitaryTrainingManagement/Controllers/ReportsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/ReportsController.cs @@ -54,13 +54,14 @@ public class ReportsController : BaseApiController public async Task GetByTargetUnit(int targetUnitId) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) { return Unauthorized(new { message = "无法获取用户组织信息" }); } // 检查访问权限 - var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, targetUnitId); + var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, targetUnitId, unitLevel.Value); if (!canAccess) { return Forbid(); @@ -79,7 +80,8 @@ public class ReportsController : BaseApiController public async Task GetById(int id) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) { return Unauthorized(new { message = "无法获取用户组织信息" }); } @@ -91,7 +93,7 @@ public class ReportsController : BaseApiController } // 检查访问权限 - var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId); + var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId, unitLevel.Value); if (!canAccess) { return Forbid(); @@ -139,7 +141,7 @@ public class ReportsController : BaseApiController } // 检查是否有权限为该单位提交上报 - var canReport = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId); + var canReport = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId, unitLevel.Value); if (!canReport) { return Forbid(); @@ -193,7 +195,7 @@ public class ReportsController : BaseApiController } // 检查是否有权限修改 - var canModify = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId); + var canModify = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId, unitLevel.Value); if (!canModify) { return Forbid(); @@ -212,19 +214,21 @@ public class ReportsController : BaseApiController /// 获取汇总数据 /// 需求4.1:师团用户查看所有下级单位的总配额、实际完成和整体完成率 /// 需求4.2:团级用户查看直接下级单位的详细数据 + /// 需求2.2:对营部及以下级别用户应用可见性过滤 /// [HttpGet("aggregated")] public async Task GetAggregatedData() { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) { return Unauthorized(new { message = "无法获取用户组织信息" }); } try { - var data = await _dataAggregatorService.CalculateHierarchicalAggregationAsync(unitId.Value); + var data = await _reportingService.GetHierarchicalAggregatedDataAsync(unitId.Value, unitLevel.Value); return Ok(data); } catch (ArgumentException ex) @@ -241,13 +245,14 @@ public class ReportsController : BaseApiController public async Task GetAggregatedDataByUnit(int targetUnitId) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) { return Unauthorized(new { message = "无法获取用户组织信息" }); } // 检查访问权限 - var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, targetUnitId); + var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, targetUnitId, unitLevel.Value); if (!canAccess) { return Forbid(); @@ -305,12 +310,13 @@ public class ReportsController : BaseApiController public async Task GetDetailedReports() { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) { return Unauthorized(new { message = "无法获取用户组织信息" }); } - var reports = await _reportingService.GetDetailedReportsAsync(unitId.Value); + var reports = await _reportingService.GetDetailedReportsAsync(unitId.Value, unitLevel.Value); return Ok(reports); } @@ -322,7 +328,8 @@ public class ReportsController : BaseApiController public async Task GetCompletionRate(int id) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var unitLevel = GetCurrentUnitLevel(); + if (unitId == null || unitLevel == null) { return Unauthorized(new { message = "无法获取用户组织信息" }); } @@ -334,7 +341,7 @@ public class ReportsController : BaseApiController } // 检查访问权限 - var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId); + var canAccess = await _reportingService.CanViewUnitDataAsync(unitId.Value, distribution.TargetUnitId, unitLevel.Value); if (!canAccess) { return Forbid(); diff --git a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs index 18ba10d..a1e6998 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs @@ -368,4 +368,52 @@ public class AllocationService : IAllocationService .OrderByDescending(r => r.ReportedAt) .ToListAsync(); } + + /// + /// 获取配额分配的上报历史记录(带可见性过滤) + /// 只返回用户可见范围内的单位的上报记录 + /// + public async Task> GetConsumptionReportsAsync(int distributionId, int userUnitId, Models.Enums.OrganizationalLevel userLevel) + { + // 获取用户可见的单位ID列表 + var visibleUnitIds = (await _organizationService.GetVisibleUnitIdsAsync(userUnitId, userLevel)).ToHashSet(); + + // 获取所有上报记录 + var allReports = await _context.ConsumptionReports + .Include(r => r.ReportedByUser) + .ThenInclude(u => u.OrganizationalUnit) + .Where(r => r.AllocationDistributionId == distributionId) + .OrderByDescending(r => r.ReportedAt) + .ToListAsync(); + + // 过滤:只返回上报人所属单位在用户可见范围内的记录 + return allReports.Where(r => visibleUnitIds.Contains(r.ReportedByUser.OrganizationalUnitId)).ToList(); + } + + /// + /// 检查用户是否有权限查看指定单位的配额分配记录 + /// 使用可见性过滤:营部及以下级别只能查看本单位及直接下级 + /// 但用户始终可以查看分配给自己单位或上级单位的记录 + /// + public async Task CanViewDistributionAsync(int userUnitId, Models.Enums.OrganizationalLevel userLevel, int targetUnitId) + { + // 用户始终可以查看分配给自己单位的记录 + if (targetUnitId == userUnitId) + { + return true; + } + + // 用户可以查看分配给上级单位的记录(因为下级可以查看上级的配额并上报消耗) + var ancestorIds = await _organizationService.GetAllAncestorIdsAsync(userUnitId); + if (ancestorIds.Contains(targetUnitId)) + { + return true; + } + + // 获取用户可见的单位ID列表(本单位 + 直接下级) + var visibleUnitIds = await _organizationService.GetVisibleUnitIdsAsync(userUnitId, userLevel); + + // 检查目标单位是否在可见范围内 + return visibleUnitIds.Contains(targetUnitId); + } } diff --git a/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs index bad23e4..9a9fb59 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/OrganizationService.cs @@ -207,4 +207,40 @@ public class OrganizationService : IOrganizationService return result; } + + /// + /// 获取用户可见的单位ID列表 + /// 营部及以下级别:本单位 + 直接下级 + /// 师团级别:本单位 + 所有下级 + /// + public async Task> GetVisibleUnitIdsAsync(int unitId, OrganizationalLevel userLevel) + { + var result = new List { unitId }; + + // 检查单位是否存在 + var unit = await _context.OrganizationalUnits.FindAsync(unitId); + if (unit == null) + { + return Enumerable.Empty(); + } + + // 营部及以下级别(Battalion = 3, Company = 4):只返回本单位 + 直接下级 + if (userLevel == OrganizationalLevel.Battalion || userLevel == OrganizationalLevel.Company) + { + var directChildren = await _context.OrganizationalUnits + .Where(u => u.ParentId == unitId) + .Select(u => u.Id) + .ToListAsync(); + + result.AddRange(directChildren); + } + // 师团级别(Division = 1, Regiment = 2):返回本单位 + 所有下级 + else + { + var allSubordinates = await GetAllSubordinateIdsAsync(unitId); + result.AddRange(allSubordinates); + } + + return result; + } } diff --git a/src/MilitaryTrainingManagement/Services/Implementations/ReportingService.cs b/src/MilitaryTrainingManagement/Services/Implementations/ReportingService.cs index 77b7d4f..cb7a85b 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/ReportingService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/ReportingService.cs @@ -65,11 +65,14 @@ public class ReportingService : IReportingService throw new ArgumentException("分配记录不存在"); // 验证上报用户是否属于目标单位或其上级 - var user = await _context.UserAccounts.FindAsync(reportedByUserId); + var user = await _context.UserAccounts + .Include(u => u.OrganizationalUnit) + .FirstOrDefaultAsync(u => u.Id == reportedByUserId); if (user == null) throw new ArgumentException("用户不存在"); - var canReport = await CanViewUnitDataAsync(user.OrganizationalUnitId, distribution.TargetUnitId); + var userLevel = user.OrganizationalUnit?.Level ?? OrganizationalLevel.Company; + var canReport = await CanViewUnitDataAsync(user.OrganizationalUnitId, distribution.TargetUnitId, userLevel); if (!canReport) throw new UnauthorizedAccessException("无权限为该单位提交上报"); @@ -107,14 +110,14 @@ public class ReportingService : IReportingService } /// - /// 获取汇总数据(包含下级单位) + /// 获取汇总数据(包含下级单位,应用可见性过滤) /// 需求3.4, 4.1:显示下级单位的汇总信息 + /// 需求2.2:对营部及以下级别用户应用可见性过滤 /// - public async Task> GetAggregatedDataAsync(int unitId) + public async Task> GetAggregatedDataAsync(int unitId, OrganizationalLevel userLevel) { - var subordinateIds = await _organizationService.GetAllSubordinateIdsAsync(unitId); - var allUnitIds = subordinateIds.ToList(); - allUnitIds.Add(unitId); + var visibleUnitIds = await _organizationService.GetVisibleUnitIdsAsync(unitId, userLevel); + var allUnitIds = visibleUnitIds.ToList(); return await _context.AllocationDistributions .Include(d => d.Allocation) @@ -134,7 +137,22 @@ public class ReportingService : IReportingService if (unit == null) throw new ArgumentException("组织单位不存在"); - var distributions = await GetAggregatedDataAsync(unitId); + // 使用单位自身的层级来确定可见性 + return await GetHierarchicalAggregatedDataAsync(unitId, unit.Level); + } + + /// + /// 获取层级汇总数据(应用可见性过滤) + /// 需求4.1, 4.2:显示总配额、实际完成和整体完成率 + /// 需求2.2:对营部及以下级别用户应用可见性过滤 + /// + public async Task GetHierarchicalAggregatedDataAsync(int unitId, OrganizationalLevel userLevel) + { + var unit = await _organizationService.GetByIdAsync(unitId); + if (unit == null) + throw new ArgumentException("组织单位不存在"); + + var distributions = await GetAggregatedDataAsync(unitId, userLevel); var distributionList = distributions.ToList(); // 按配额分组汇总 @@ -179,7 +197,11 @@ public class ReportingService : IReportingService /// public async Task> GetAggregatedByAllocationAsync(int unitId) { - var distributions = await GetAggregatedDataAsync(unitId); + // 获取单位信息以确定可见性 + var unit = await _organizationService.GetByIdAsync(unitId); + var userLevel = unit?.Level ?? OrganizationalLevel.Division; + + var distributions = await GetAggregatedDataAsync(unitId, userLevel); var distributionList = distributions.ToList(); return distributionList @@ -261,24 +283,25 @@ public class ReportingService : IReportingService /// /// 检查用户是否有权限查看指定单位的数据 /// 需求4.4, 8.1, 8.2:基于组织层级的数据访问控制 + /// 需求2.3:使用可见性范围检查 /// - public async Task CanViewUnitDataAsync(int userUnitId, int targetUnitId) + public async Task CanViewUnitDataAsync(int userUnitId, int targetUnitId, OrganizationalLevel userLevel) { - // 同一单位可以查看 - if (userUnitId == targetUnitId) - return true; - - // 检查目标单位是否是用户单位的下级 - return await _organizationService.IsUnitOrSubordinateAsync(userUnitId, targetUnitId); + // 获取用户可见的单位ID列表 + var visibleUnitIds = await _organizationService.GetVisibleUnitIdsAsync(userUnitId, userLevel); + + // 检查目标单位是否在可见范围内 + return visibleUnitIds.Contains(targetUnitId); } /// /// 获取详细上报记录 /// 需求4.3:提供具体上报记录的访问,包括时间戳和上报人员 + /// 需求2.1:对营部及以下级别用户应用可见性过滤 /// - public async Task> GetDetailedReportsAsync(int unitId) + public async Task> GetDetailedReportsAsync(int unitId, OrganizationalLevel userLevel) { - var distributions = await GetAggregatedDataAsync(unitId); + var distributions = await GetAggregatedDataAsync(unitId, userLevel); return distributions.Select(d => new ReportResponse { diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs index 13d23c6..9b483a4 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs @@ -1,4 +1,5 @@ using MilitaryTrainingManagement.Models.Entities; +using MilitaryTrainingManagement.Models.Enums; namespace MilitaryTrainingManagement.Services.Interfaces; @@ -76,4 +77,16 @@ public interface IAllocationService /// 获取配额分配的上报历史记录 /// Task> GetConsumptionReportsAsync(int distributionId); + + /// + /// 获取配额分配的上报历史记录(带可见性过滤) + /// 只返回用户可见范围内的单位的上报记录 + /// + Task> GetConsumptionReportsAsync(int distributionId, int userUnitId, OrganizationalLevel userLevel); + + /// + /// 检查用户是否有权限查看指定单位的配额分配记录 + /// 使用可见性过滤:营部及以下级别只能查看本单位及直接下级 + /// + Task CanViewDistributionAsync(int userUnitId, OrganizationalLevel userLevel, int targetUnitId); } diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs index 82814f7..31f9923 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IOrganizationService.cs @@ -19,4 +19,14 @@ public interface IOrganizationService Task IsSubordinateOfAsync(int unitId, int potentialParentId); Task IsUnitOrSubordinateAsync(int userUnitId, int targetUnitId); Task IsParentUnitAsync(int parentUnitId, int childUnitId); + + /// + /// 获取用户可见的单位ID列表 + /// 营部及以下级别:本单位 + 直接下级 + /// 师团级别:本单位 + 所有下级 + /// + /// 用户所属单位ID + /// 用户的组织层级 + /// 可见的单位ID列表 + Task> GetVisibleUnitIdsAsync(int unitId, OrganizationalLevel userLevel); } diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IReportingService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IReportingService.cs index 6d6aa5c..3288575 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IReportingService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IReportingService.cs @@ -1,5 +1,6 @@ using MilitaryTrainingManagement.Models.DTOs; using MilitaryTrainingManagement.Models.Entities; +using MilitaryTrainingManagement.Models.Enums; namespace MilitaryTrainingManagement.Services.Interfaces; @@ -34,15 +35,20 @@ public interface IReportingService decimal CalculateCompletionRate(decimal? actualCompletion, decimal unitQuota); /// - /// 获取汇总数据(包含下级单位) + /// 获取汇总数据(包含下级单位,应用可见性过滤) /// - Task> GetAggregatedDataAsync(int unitId); + Task> GetAggregatedDataAsync(int unitId, OrganizationalLevel userLevel); /// /// 获取层级汇总数据 /// Task GetHierarchicalAggregatedDataAsync(int unitId); + /// + /// 获取层级汇总数据(应用可见性过滤) + /// + Task GetHierarchicalAggregatedDataAsync(int unitId, OrganizationalLevel userLevel); + /// /// 获取按配额分组的汇总数据 /// @@ -54,12 +60,12 @@ public interface IReportingService Task> GetDirectSubordinateSummariesAsync(int unitId); /// - /// 检查用户是否有权限查看指定单位的数据 + /// 检查用户是否有权限查看指定单位的数据(使用可见性范围) /// - Task CanViewUnitDataAsync(int userUnitId, int targetUnitId); + Task CanViewUnitDataAsync(int userUnitId, int targetUnitId, OrganizationalLevel userLevel); /// - /// 获取详细上报记录(包含时间戳和上报人员) + /// 获取详细上报记录(包含时间戳和上报人员,应用可见性过滤) /// - Task> GetDetailedReportsAsync(int unitId); + Task> GetDetailedReportsAsync(int unitId, OrganizationalLevel userLevel); } diff --git a/src/frontend/src/views/allocations/AllocationReport.vue b/src/frontend/src/views/allocations/AllocationReport.vue index 0312363..14cfcaa 100644 --- a/src/frontend/src/views/allocations/AllocationReport.vue +++ b/src/frontend/src/views/allocations/AllocationReport.vue @@ -37,15 +37,17 @@ {{ distribution.targetUnitName }} - + + {{ formatNumber(distribution.unitQuota) }} {{ allocation?.unit }} - - {{ distribution.actualCompletion ? formatNumber(distribution.actualCompletion) : '0' }} {{ allocation?.unit }} + + {{ totalReportedAmount ? formatNumber(totalReportedAmount) : '0' }} {{ allocation?.unit }} - + + {{ formatNumber((distribution.unitQuota || 0) - (distribution.actualCompletion || 0)) }} {{ allocation?.unit }} @@ -69,7 +71,7 @@ @@ -77,7 +79,7 @@ {{ allocation?.unit }} - + +
{{ formatNumber((distribution.actualCompletion || 0) + (form.actualCompletion || 0)) }} {{ allocation?.unit }} @@ -95,7 +98,8 @@
- + +
{ + return authStore.organizationalLevelNum >= 3 +}) + +// 计算本单位和下属单位的上报数之和 +const totalReportedAmount = computed(() => { + if (consumptionReports.value.length === 0) { + return distribution.value?.actualCompletion || 0 + } + return consumptionReports.value.reduce((sum, report) => sum + report.reportedAmount, 0) +}) + +// 根据用户级别动态生成验证规则 +const formRules = computed(() => { + const baseRules: FormRules = { + actualCompletion: [ + { required: true, message: '请输入本次上报数量', trigger: 'blur' }, + { + type: 'number', + min: 0.01, + message: '数量必须大于0', + trigger: 'blur' + } + ] + } + + // 只有师团/团级才需要验证不超过剩余配额 + if (!isBattalionOrBelow.value) { + baseRules.actualCompletion.push({ validator: (_rule, value, callback) => { const remaining = (distribution.value?.unitQuota || 0) - (distribution.value?.actualCompletion || 0) if (value > remaining) { @@ -221,9 +247,11 @@ const rules: FormRules = { } }, trigger: 'blur' - } - ] -} + }) + } + + return baseRules +}) function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleString('zh-CN') @@ -268,8 +296,13 @@ async function handleSubmit() { const newTotal = (distribution.value?.actualCompletion || 0) + form.actualCompletion + // 营部及以下级别的确认信息不显示总数 + const confirmMessage = isBattalionOrBelow.value + ? `本次上报数量:${form.actualCompletion} ${allocation.value?.unit}\n\n确认提交吗?` + : `本次上报数量:${form.actualCompletion} ${allocation.value?.unit}\n上报后总数:${newTotal} ${allocation.value?.unit}\n\n确认提交吗?` + await ElMessageBox.confirm( - `本次上报数量:${form.actualCompletion} ${allocation.value?.unit}\n上报后总数:${newTotal} ${allocation.value?.unit}\n\n确认提交吗?`, + confirmMessage, '确认上报', { type: 'warning',