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>
|
/// <returns></returns>
|
||||||
Task ModifyClass(ModifySportsHallClassParam paramDto);
|
Task ModifyClass(ModifySportsHallClassParam paramDto);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量删除当前学校的运动馆空班级
|
||||||
|
/// </summary>
|
||||||
|
Task<int> BatchDeleteClasses(List<int> ids);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 导入运动馆学生体测成绩
|
/// 导入运动馆学生体测成绩
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -440,6 +440,17 @@ namespace VOL.Business.Services.School
|
|||||||
if (UserContext.Current.RoleId != 1 && UserContext.Current.RoleId != 2)
|
if (UserContext.Current.RoleId != 1 && UserContext.Current.RoleId != 2)
|
||||||
throw new Exception("仅管理员可以批量删除学生");
|
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>();
|
var studentIds = ids?.Where(x => x > 0).Distinct().ToList() ?? new List<int>();
|
||||||
if (studentIds.Count == 0)
|
if (studentIds.Count == 0)
|
||||||
throw new ArgumentException("请至少选择一名学生");
|
throw new ArgumentException("请至少选择一名学生");
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using Newtonsoft.Json;
|
|||||||
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime;
|
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -40,6 +41,7 @@ namespace VOL.Business.Services.SportsHall
|
|||||||
private readonly IS_GradeRepository _gradeRepository;
|
private readonly IS_GradeRepository _gradeRepository;
|
||||||
private readonly IS_ClassAssocTeacherRepository _classAssocTeacherRepository;
|
private readonly IS_ClassAssocTeacherRepository _classAssocTeacherRepository;
|
||||||
private readonly IS_StudentRepository _studentRepository;
|
private readonly IS_StudentRepository _studentRepository;
|
||||||
|
private readonly IotDataSyncService _iotDataSyncService;
|
||||||
IN_HealthStandardsRepository _healthStandardsRepository;
|
IN_HealthStandardsRepository _healthStandardsRepository;
|
||||||
IN_SportsTestResultRepository _sportsTestResultRepository;
|
IN_SportsTestResultRepository _sportsTestResultRepository;
|
||||||
|
|
||||||
@ -48,6 +50,7 @@ namespace VOL.Business.Services.SportsHall
|
|||||||
IS_GradeRepository gradeRepository,
|
IS_GradeRepository gradeRepository,
|
||||||
IS_ClassAssocTeacherRepository classAssocTeacherRepository,
|
IS_ClassAssocTeacherRepository classAssocTeacherRepository,
|
||||||
IS_StudentRepository studentRepository,
|
IS_StudentRepository studentRepository,
|
||||||
|
IotDataSyncService iotDataSyncService,
|
||||||
IN_HealthStandardsRepository healthStandardsRepository,
|
IN_HealthStandardsRepository healthStandardsRepository,
|
||||||
IN_SportsTestResultRepository sportsTestResultRepository)
|
IN_SportsTestResultRepository sportsTestResultRepository)
|
||||||
{
|
{
|
||||||
@ -55,6 +58,7 @@ namespace VOL.Business.Services.SportsHall
|
|||||||
_gradeRepository = gradeRepository;
|
_gradeRepository = gradeRepository;
|
||||||
_classAssocTeacherRepository = classAssocTeacherRepository;
|
_classAssocTeacherRepository = classAssocTeacherRepository;
|
||||||
_studentRepository = studentRepository;
|
_studentRepository = studentRepository;
|
||||||
|
_iotDataSyncService = iotDataSyncService;
|
||||||
_healthStandardsRepository = healthStandardsRepository;
|
_healthStandardsRepository = healthStandardsRepository;
|
||||||
_sportsTestResultRepository = sportsTestResultRepository;
|
_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>
|
||||||
/// 导入运动馆学生体测成绩
|
/// 导入运动馆学生体测成绩
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using VOL.Business.IServices.School;
|
using VOL.Business.IServices.School;
|
||||||
using VOL.Business.IServices.SportsHall;
|
using VOL.Business.IServices.SportsHall;
|
||||||
@ -54,6 +55,16 @@ namespace VOL.WebApi.Controllers.SportsHall
|
|||||||
return Ok("更新成功");
|
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>
|
/// <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