feat: 运动馆班级批量删除与菜单权限登记
新增 SportsHall/BatchDeleteClasses 接口:校验批量数量、本校运动馆班级归属及空班条件,在可串行化事务中清理班级和教练关联,并发送物联网删除同步。 学生批量删除增加 BatchDelete 动作权限校验;按实际页面菜单匹配权限,兼容生产库中重复的学生管理菜单。运动馆班级删除由已授权角色使用,不硬编码学校角色。 新增幂等菜单权限脚本,仅向学生管理和运动馆班级管理的有效页面菜单登记批量删除动作,不自动给角色授权。QA 与生产库已核验菜单动作;代码上线仍需独立部署。 验证:Debug/Release 构建通过,SQL 在 QA 重跑无重复写入。
This commit is contained in:
parent
c4b01d2f53
commit
48151578a1
@ -27,6 +27,11 @@ namespace VOL.Business.IServices.SportsHall
|
||||
/// <returns></returns>
|
||||
Task ModifyClass(ModifySportsHallClassParam paramDto);
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除当前学校的运动馆空班级
|
||||
/// </summary>
|
||||
Task<int> BatchDeleteClasses(List<int> ids);
|
||||
|
||||
/// <summary>
|
||||
/// 导入运动馆学生体测成绩
|
||||
/// </summary>
|
||||
|
||||
@ -440,6 +440,17 @@ namespace VOL.Business.Services.School
|
||||
if (UserContext.Current.RoleId != 1 && UserContext.Current.RoleId != 2)
|
||||
throw new Exception("仅管理员可以批量删除学生");
|
||||
|
||||
if (UserContext.Current.RoleId != 1)
|
||||
{
|
||||
var menuIds = await _studentRepository.DbContext.Set<Sys_Menu>()
|
||||
.Where(menu => menu.Url == "/studentsManage" && menu.ParentId > 0)
|
||||
.Select(menu => menu.Menu_Id)
|
||||
.ToListAsync();
|
||||
if (!UserContext.Current.Permissions.Any(permission =>
|
||||
menuIds.Contains(permission.Menu_Id) && permission.UserAuthArr.Contains("BatchDelete")))
|
||||
throw new Exception("没有批量删除学生的权限");
|
||||
}
|
||||
|
||||
var studentIds = ids?.Where(x => x > 0).Distinct().ToList() ?? new List<int>();
|
||||
if (studentIds.Count == 0)
|
||||
throw new ArgumentException("请至少选择一名学生");
|
||||
|
||||
@ -6,6 +6,7 @@ using Newtonsoft.Json;
|
||||
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@ -40,6 +41,7 @@ namespace VOL.Business.Services.SportsHall
|
||||
private readonly IS_GradeRepository _gradeRepository;
|
||||
private readonly IS_ClassAssocTeacherRepository _classAssocTeacherRepository;
|
||||
private readonly IS_StudentRepository _studentRepository;
|
||||
private readonly IotDataSyncService _iotDataSyncService;
|
||||
IN_HealthStandardsRepository _healthStandardsRepository;
|
||||
IN_SportsTestResultRepository _sportsTestResultRepository;
|
||||
|
||||
@ -48,6 +50,7 @@ namespace VOL.Business.Services.SportsHall
|
||||
IS_GradeRepository gradeRepository,
|
||||
IS_ClassAssocTeacherRepository classAssocTeacherRepository,
|
||||
IS_StudentRepository studentRepository,
|
||||
IotDataSyncService iotDataSyncService,
|
||||
IN_HealthStandardsRepository healthStandardsRepository,
|
||||
IN_SportsTestResultRepository sportsTestResultRepository)
|
||||
{
|
||||
@ -55,6 +58,7 @@ namespace VOL.Business.Services.SportsHall
|
||||
_gradeRepository = gradeRepository;
|
||||
_classAssocTeacherRepository = classAssocTeacherRepository;
|
||||
_studentRepository = studentRepository;
|
||||
_iotDataSyncService = iotDataSyncService;
|
||||
_healthStandardsRepository = healthStandardsRepository;
|
||||
_sportsTestResultRepository = sportsTestResultRepository;
|
||||
}
|
||||
@ -221,6 +225,68 @@ namespace VOL.Business.Services.SportsHall
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> BatchDeleteClasses(List<int> ids)
|
||||
{
|
||||
var user = UserContext.Current;
|
||||
var classIds = ids?.Where(id => id > 0).Distinct().ToList() ?? new List<int>();
|
||||
if (classIds.Count == 0 || classIds.Count > 100)
|
||||
throw new ArgumentException("请选择1至100个班级");
|
||||
|
||||
if (user.RoleId != 1)
|
||||
{
|
||||
var menuIds = await _classRepository.DbContext.Set<Sys_Menu>()
|
||||
.Where(menu => menu.Url == "/sportsHallClass" && menu.ParentId > 0)
|
||||
.Select(menu => menu.Menu_Id)
|
||||
.ToListAsync();
|
||||
if (!user.Permissions.Any(permission =>
|
||||
menuIds.Contains(permission.Menu_Id) && permission.UserAuthArr.Contains("BatchDelete")))
|
||||
throw new Exception("没有批量删除运动馆班级的权限");
|
||||
}
|
||||
|
||||
var gradeId = await _gradeRepository.FindAsIQueryable(grade => grade.GradeName == "运动馆")
|
||||
.Select(grade => grade.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (gradeId == 0)
|
||||
throw new Exception("未找到运动馆年级");
|
||||
|
||||
await using var transaction = await _classRepository.DbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
var classes = await _classRepository.DbContext.Set<S_Class>()
|
||||
.Where(item => item.SchoolCode == user.TenantId && item.GradeId == gradeId && classIds.Contains(item.Id))
|
||||
.ToListAsync();
|
||||
if (classes.Count != classIds.Count)
|
||||
throw new Exception("部分班级不存在或不属于当前学校,请刷新列表后重试");
|
||||
|
||||
var occupiedClassIds = await _classRepository.DbContext.Set<S_Student>()
|
||||
.Where(student => classIds.Contains(student.ClassId))
|
||||
.Select(student => student.ClassId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
if (occupiedClassIds.Count > 0)
|
||||
{
|
||||
var names = classes.Where(item => occupiedClassIds.Contains(item.Id)).Select(item => item.ClassName);
|
||||
throw new Exception($"班级 {string.Join("、", names)} 仍有学生,请先调整学生后再删除");
|
||||
}
|
||||
|
||||
var teacherAssociations = await _classRepository.DbContext.Set<S_ClassAssocTeacher>()
|
||||
.Where(item => classIds.Contains(item.ClassId))
|
||||
.ToListAsync();
|
||||
_classRepository.DbContext.RemoveRange(teacherAssociations);
|
||||
_classRepository.DbContext.RemoveRange(classes);
|
||||
await _classRepository.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
foreach (var item in classes)
|
||||
{
|
||||
await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam
|
||||
{
|
||||
EventType = EventType.Delete.GetDisplayName(),
|
||||
DataType = IOTDataSyncType.Class.GetDisplayName(),
|
||||
Json = JsonConvert.SerializeObject(item)
|
||||
});
|
||||
}
|
||||
return classes.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入运动馆学生体测成绩
|
||||
/// </summary>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using VOL.Business.IServices.School;
|
||||
using VOL.Business.IServices.SportsHall;
|
||||
@ -54,6 +55,16 @@ namespace VOL.WebApi.Controllers.SportsHall
|
||||
return Ok("更新成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除当前学校的运动馆空班级
|
||||
/// </summary>
|
||||
[HttpPost(nameof(BatchDeleteClasses))]
|
||||
public async Task<ActionResult> BatchDeleteClasses([FromBody] List<int> ids)
|
||||
{
|
||||
var deletedCount = await _sportsHallService.BatchDeleteClasses(ids);
|
||||
return Ok($"成功删除{deletedCount}个班级");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 导入运动馆学生体测数据
|
||||
|
||||
49
batch-delete-menu-permissions.sql
Normal file
49
batch-delete-menu-permissions.sql
Normal file
@ -0,0 +1,49 @@
|
||||
SET XACT_ABORT ON;
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @menuUrl nvarchar(100);
|
||||
DECLARE @menuId int;
|
||||
DECLARE menu_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT Url FROM (VALUES (N'/studentsManage'), (N'/sportsHallClass')) AS menus(Url);
|
||||
|
||||
OPEN menu_cursor;
|
||||
FETCH NEXT FROM menu_cursor INTO @menuUrl;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM Sys_Menu
|
||||
WHERE Url = @menuUrl AND ParentId > 0 AND MenuType = 0 AND Enable IN (1, 2)) <> 1
|
||||
THROW 51001, N'页面菜单不存在或不唯一,请检查菜单配置', 1;
|
||||
|
||||
SELECT @menuId = Menu_Id FROM Sys_Menu
|
||||
WHERE Url = @menuUrl AND ParentId > 0 AND MenuType = 0 AND Enable IN (1, 2);
|
||||
|
||||
IF EXISTS (SELECT 1 FROM Sys_Menu WHERE Menu_Id = @menuId
|
||||
AND NULLIF(LTRIM(RTRIM(Auth)), N'') IS NOT NULL
|
||||
AND (ISJSON(Auth) <> 1 OR LEFT(LTRIM(Auth), 1) <> N'['))
|
||||
THROW 51002, N'菜单权限不是有效 JSON 数组,请先检查原始数据', 1;
|
||||
|
||||
UPDATE Sys_Menu
|
||||
SET Auth = COALESCE(NULLIF(Auth, N''), N'[]')
|
||||
WHERE Menu_Id = @menuId AND (Auth IS NULL OR Auth = N'');
|
||||
|
||||
UPDATE menu
|
||||
SET Auth = JSON_MODIFY(menu.Auth, 'append $',
|
||||
JSON_QUERY(N'{"text":"批量删除","value":"BatchDelete"}'))
|
||||
FROM Sys_Menu AS menu
|
||||
WHERE menu.Menu_Id = @menuId
|
||||
AND NOT EXISTS (SELECT 1 FROM OPENJSON(menu.Auth)
|
||||
WITH (Value nvarchar(100) '$.value') AS action
|
||||
WHERE action.Value = N'BatchDelete');
|
||||
|
||||
FETCH NEXT FROM menu_cursor INTO @menuUrl;
|
||||
END;
|
||||
CLOSE menu_cursor;
|
||||
DEALLOCATE menu_cursor;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
SELECT MenuName, Url, Auth
|
||||
FROM Sys_Menu
|
||||
WHERE Url IN (N'/studentsManage', N'/sportsHallClass')
|
||||
AND ParentId > 0 AND MenuType = 0;
|
||||
GO
|
||||
Loading…
x
Reference in New Issue
Block a user