ltx e049955f9c feat: 完善学生数据管理及作业批量导出
- 完善学生Excel导入的文件、行数据和重复学号校验,并返回实际导入数量
- 调整学生导出字段名称与排列顺序,移除学籍号字段
- 规范单张及ZIP批量人脸导入校验,限制格式、大小、数量和解压总量
- 批量人脸导入返回总数、成功数、失败数及详细失败原因
- 新增作业按作业、班级、完成状态等条件批量导出,每名学生一条记录
2026-07-31 14:21:04 +08:00

624 lines
24 KiB
C#

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VOL.Business.IServices.Device;
using VOL.Core.EFDbContext;
using VOL.Core.Extensions.AutofacManager;
using VOL.Core.ManageUser;
using VOL.Entity.DomainModels;
using VOL.Entity.DomainModels.XinWei;
using VOL.Entity.DomainModels.YiLu;
using VOL.Entity.Enum;
using VOL.Model;
using VOL.Model.Device.Request;
using VOL.Model.Device.Response;
namespace VOL.Business.Services.Device
{
/// <summary>
/// 学校设备管理服务。
/// </summary>
public class DeviceManageService : IDeviceManageService, IDependency
{
private readonly VOLContext _dbContext;
public DeviceManageService(VOLContext dbContext)
{
_dbContext = dbContext;
}
public async Task<PageDataDto<DeviceManageModel>> GetPageList(DevicePageListParam param)
{
EnsureSourceType(param.SourceType);
var devices = new List<DeviceManageModel>();
if (!param.SourceType.HasValue || param.SourceType == DeviceSourceType.AiDevice)
devices.AddRange(await QueryAiDevices(param));
if (!param.SourceType.HasValue || param.SourceType == DeviceSourceType.YiLuDevice)
devices.AddRange(await QueryYiLuDevices(param));
if (!param.SourceType.HasValue || param.SourceType == DeviceSourceType.XinWeiDevice)
devices.AddRange(await QueryXinWeiDevices(param));
var orderedDevices = devices
.OrderByDescending(x => x.UpdateTime ?? x.CreateTime)
.ThenByDescending(x => x.Id)
.ToList();
var total = orderedDevices.Count;
return new PageDataDto<DeviceManageModel>
{
Total = total,
TotalPages = (int)Math.Ceiling(total / (double)param.PageSize),
PageIndex = param.PageIndex,
PageSize = param.PageSize,
Datas = orderedDevices
.Skip((param.PageIndex - 1) * param.PageSize)
.Take(param.PageSize)
.ToList()
};
}
public async Task<List<DeviceSchoolOption>> GetSchoolOptions(string keyword)
{
var query = GetAllowedSchools();
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
query = query.Where(x =>
x.SchoolName.Contains(keyword) ||
x.SchoolCode.Contains(keyword));
}
return await query
.OrderBy(x => x.SchoolName)
.Select(x => new DeviceSchoolOption
{
OrgId = x.Id,
OrgName = x.SchoolName,
SchoolCode = x.SchoolCode
})
.ToListAsync();
}
public async Task<DeviceManageModel> Details(long id, DeviceSourceType sourceType)
{
EnsureSourceType(sourceType);
DeviceManageModel result;
switch (sourceType)
{
case DeviceSourceType.AiDevice:
result = (await QueryAiDevices(new DevicePageListParam
{
PageIndex = 1,
PageSize = 1
})).FirstOrDefault(x => x.Id == id);
break;
case DeviceSourceType.YiLuDevice:
result = (await QueryYiLuDevices(new DevicePageListParam
{
PageIndex = 1,
PageSize = 1
})).FirstOrDefault(x => x.Id == id);
break;
default:
result = (await QueryXinWeiDevices(new DevicePageListParam
{
PageIndex = 1,
PageSize = 1
})).FirstOrDefault(x => x.Id == id);
break;
}
return result ?? throw new Exception("设备不存在或当前账号无权访问");
}
public async Task Add(SaveDeviceParam param)
{
NormalizeAndValidate(param);
var school = await GetAllowedSchool(param.OrgId);
switch (param.SourceType)
{
case DeviceSourceType.AiDevice:
await AddAiDevice(param, school);
break;
case DeviceSourceType.YiLuDevice:
await AddYiLuDevice(param, school);
break;
case DeviceSourceType.XinWeiDevice:
await AddXinWeiDevice(param, school);
break;
default:
throw new Exception("设备类型无效");
}
}
public async Task Update(SaveDeviceParam param)
{
NormalizeAndValidate(param);
if (!param.Id.HasValue || param.Id <= 0)
throw new Exception("设备Id不能为空");
var school = await GetAllowedSchool(param.OrgId);
switch (param.SourceType)
{
case DeviceSourceType.AiDevice:
await UpdateAiDevice(param, school);
break;
case DeviceSourceType.YiLuDevice:
await UpdateYiLuDevice(param, school);
break;
case DeviceSourceType.XinWeiDevice:
await UpdateXinWeiDevice(param, school);
break;
default:
throw new Exception("设备类型无效");
}
}
public async Task Delete(DeleteDeviceParam param)
{
EnsureSourceType(param.SourceType);
if (param.Id <= 0)
throw new Exception("设备Id无效");
try
{
switch (param.SourceType)
{
case DeviceSourceType.AiDevice:
await DeleteAiDevice(param.Id);
break;
case DeviceSourceType.YiLuDevice:
await DeleteYiLuDevice(param.Id);
break;
case DeviceSourceType.XinWeiDevice:
await DeleteXinWeiDevice(param.Id);
break;
}
}
catch (DbUpdateException)
{
throw new Exception("该设备已产生关联业务数据,暂时无法删除");
}
}
private IQueryable<S_School> GetAllowedSchools()
{
var query = _dbContext.Set<S_School>().AsNoTracking();
if (!UserContext.Current.IsSuperAdmin)
{
var tenantId = UserContext.Current.TenantId;
query = query.Where(x => x.SchoolCode == tenantId);
}
return query;
}
private async Task<S_School> GetAllowedSchool(int orgId)
{
var school = await GetAllowedSchools().FirstOrDefaultAsync(x => x.Id == orgId);
return school ?? throw new Exception("学校不存在或当前账号无权操作");
}
private async Task<List<DeviceManageModel>> QueryAiDevices(DevicePageListParam param)
{
var allowedSchoolIds = GetAllowedSchools().Select(x => x.Id);
var query =
from device in _dbContext.Set<Ai_Device>().AsNoTracking()
join association in _dbContext.Set<Ai_DeviceAssocSchool>().AsNoTracking()
on device.Code equals association.Code into associationGroup
from association in associationGroup.DefaultIfEmpty()
join school in _dbContext.Set<S_School>().AsNoTracking()
on association.SchoolCode equals school.SchoolCode into schoolGroup
from school in schoolGroup.DefaultIfEmpty()
where UserContext.Current.IsSuperAdmin ||
(school != null && allowedSchoolIds.Contains(school.Id))
select new DeviceManageModel
{
Id = device.Id,
SourceType = DeviceSourceType.AiDevice,
SourceTypeName = "一体机设备",
OrgId = school == null ? null : (int?)school.Id,
OrgName = school == null ? null : school.SchoolName,
SerialNumber = device.Code,
TenantName = device.Name,
DeviceType = device.DeviceType.ToString(),
CreateTime = device.CreateDate,
UpdateTime = device.ModifyDate
};
query = ApplyFilters(query, param);
return await query.ToListAsync();
}
private async Task<List<DeviceManageModel>> QueryYiLuDevices(DevicePageListParam param)
{
var allowedSchoolIds = GetAllowedSchools().Select(x => x.Id);
var query =
from device in _dbContext.Set<YL_Device>().AsNoTracking()
join school in _dbContext.Set<S_School>().AsNoTracking()
on device.OrgId equals school.Id into schoolGroup
from school in schoolGroup.DefaultIfEmpty()
where UserContext.Current.IsSuperAdmin ||
(device.OrgId.HasValue && allowedSchoolIds.Contains(device.OrgId.Value))
select new DeviceManageModel
{
Id = device.Id,
SourceType = DeviceSourceType.YiLuDevice,
SourceTypeName = "壹路设备",
OrgId = device.OrgId,
OrgName = school == null ? null : school.SchoolName,
SerialNumber = device.SerialNumber,
TenantName = device.TenantName ?? device.Name,
CreateTime = device.CreateTime,
UpdateTime = device.UpdateTime
};
query = ApplyFilters(query, param);
return await query.ToListAsync();
}
private async Task<List<DeviceManageModel>> QueryXinWeiDevices(DevicePageListParam param)
{
var allowedSchoolIds = GetAllowedSchools().Select(x => x.Id);
var query = _dbContext.Set<XW_Device>()
.AsNoTracking()
.Where(x => UserContext.Current.IsSuperAdmin ||
(x.OrgId.HasValue && allowedSchoolIds.Contains(x.OrgId.Value)));
if (param.OrgId.HasValue && param.OrgId > 0)
query = query.Where(x => x.OrgId == param.OrgId);
if (!string.IsNullOrWhiteSpace(param.Keyword))
{
var keyword = param.Keyword.Trim();
query = query.Where(x =>
(x.DeviceSerial != null && x.DeviceSerial.Contains(keyword)) ||
(x.Remark != null && x.Remark.Contains(keyword)) ||
(x.OrgName != null && x.OrgName.Contains(keyword)) ||
(x.CameraIp != null && x.CameraIp.Contains(keyword)));
}
var rows = await query
.Select(x => new
{
x.Id,
x.OrgId,
x.OrgName,
x.DeviceSerial,
x.Remark,
x.CameraIp,
x.Eai100Flag,
x.DeviceType
})
.ToListAsync();
return rows.Select(x => new DeviceManageModel
{
Id = x.Id,
SourceType = DeviceSourceType.XinWeiDevice,
SourceTypeName = "芯未设备",
OrgId = x.OrgId,
OrgName = x.OrgName,
SerialNumber = x.DeviceSerial,
TenantName = x.Remark,
CameraIp = x.CameraIp,
Eai100Flag = x.Eai100Flag,
DeviceType = x.DeviceType
}).ToList();
}
private static IQueryable<DeviceManageModel> ApplyFilters(
IQueryable<DeviceManageModel> query,
DevicePageListParam param)
{
if (param.OrgId.HasValue && param.OrgId > 0)
query = query.Where(x => x.OrgId == param.OrgId);
if (!string.IsNullOrWhiteSpace(param.Keyword))
{
var keyword = param.Keyword.Trim();
query = query.Where(x =>
(x.SerialNumber != null && x.SerialNumber.Contains(keyword)) ||
(x.TenantName != null && x.TenantName.Contains(keyword)) ||
(x.OrgName != null && x.OrgName.Contains(keyword)) ||
(x.CameraIp != null && x.CameraIp.Contains(keyword)));
}
return query;
}
private async Task AddAiDevice(SaveDeviceParam param, S_School school)
{
if (await _dbContext.Set<Ai_Device>().AnyAsync(x => x.Code == param.SerialNumber))
throw new Exception($"设备编号{param.SerialNumber}已存在");
var now = DateTime.Now;
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
_dbContext.Set<Ai_Device>().Add(new Ai_Device
{
Code = param.SerialNumber,
Name = param.TenantName,
DeviceType = 1,
Status = 1,
StartTime = now,
EndTime = new DateTime(2099, 12, 31),
Creator = UserContext.Current.UserId,
CreateDate = now,
Modifier = 0
});
_dbContext.Set<Ai_DeviceAssocSchool>().Add(new Ai_DeviceAssocSchool
{
Code = param.SerialNumber,
SchoolCode = school.SchoolCode
});
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
private async Task AddYiLuDevice(SaveDeviceParam param, S_School school)
{
if (await _dbContext.Set<YL_Device>().AnyAsync(x => x.SerialNumber == param.SerialNumber))
throw new Exception($"设备编号{param.SerialNumber}已存在");
var now = DateTime.Now;
_dbContext.Set<YL_Device>().Add(new YL_Device
{
SportStationId = 0,
OrgId = school.Id,
Name = param.TenantName,
TenantName = param.TenantName,
SerialNumber = param.SerialNumber,
IsCalibration = 0,
IsSynchronization = 0,
IsActive = 0,
IsOnline = 0,
CreateTime = now
});
await _dbContext.SaveChangesAsync();
}
private async Task AddXinWeiDevice(SaveDeviceParam param, S_School school)
{
if (await _dbContext.Set<XW_Device>().AnyAsync(x => x.DeviceSerial == param.SerialNumber))
throw new Exception($"设备编号{param.SerialNumber}已存在");
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
_dbContext.Set<XW_Device>().Add(new XW_Device
{
Type = 1,
DeviceType = "1",
DeviceSerial = param.SerialNumber,
CameraIp = param.CameraIp,
Eai100Flag = param.Eai100Flag ?? false,
OrgId = school.Id,
OrgName = school.SchoolName,
Remark = param.TenantName,
State = 1,
Status = 1,
CreateTime = now,
CreateBy = UserContext.Current.UserInfo.UserName,
SimilarNumber = 80,
CameraInfo = string.Empty
});
await _dbContext.SaveChangesAsync();
}
private async Task UpdateAiDevice(SaveDeviceParam param, S_School school)
{
var id = ToIntId(param.Id.Value);
var device = await _dbContext.Set<Ai_Device>()
.AsTracking()
.FirstOrDefaultAsync(x => x.Id == id);
if (device == null)
throw new Exception("一体机设备不存在");
await EnsureAiDevicePermission(device.Code);
if (await _dbContext.Set<Ai_Device>()
.AnyAsync(x => x.Code == param.SerialNumber && x.Id != id))
throw new Exception($"设备编号{param.SerialNumber}已存在");
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
var association = await _dbContext.Set<Ai_DeviceAssocSchool>()
.AsTracking()
.FirstOrDefaultAsync(x => x.Code == device.Code);
device.Code = param.SerialNumber;
device.Name = param.TenantName;
device.Modifier = UserContext.Current.UserId;
device.ModifyDate = DateTime.Now;
if (association == null)
{
association = new Ai_DeviceAssocSchool
{
Code = param.SerialNumber
};
_dbContext.Set<Ai_DeviceAssocSchool>().Add(association);
}
association.Code = param.SerialNumber;
association.SchoolCode = school.SchoolCode;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
private async Task UpdateYiLuDevice(SaveDeviceParam param, S_School school)
{
var device = await _dbContext.Set<YL_Device>()
.AsTracking()
.FirstOrDefaultAsync(x => x.Id == param.Id.Value);
if (device == null)
throw new Exception("壹路设备不存在");
await EnsureOrgPermission(device.OrgId);
if (await _dbContext.Set<YL_Device>()
.AnyAsync(x => x.SerialNumber == param.SerialNumber && x.Id != param.Id.Value))
throw new Exception($"设备编号{param.SerialNumber}已存在");
device.OrgId = school.Id;
device.Name = param.TenantName;
device.TenantName = param.TenantName;
device.SerialNumber = param.SerialNumber;
device.UpdateTime = DateTime.Now;
await _dbContext.SaveChangesAsync();
}
private async Task UpdateXinWeiDevice(SaveDeviceParam param, S_School school)
{
var id = ToIntId(param.Id.Value);
var device = await _dbContext.Set<XW_Device>()
.AsNoTracking()
.Where(x => x.Id == id)
.Select(x => new
{
x.Id,
x.OrgId
})
.FirstOrDefaultAsync();
if (device == null)
throw new Exception("芯未设备不存在");
await EnsureOrgPermission(device.OrgId);
if (await _dbContext.Set<XW_Device>()
.AnyAsync(x => x.DeviceSerial == param.SerialNumber && x.Id != id))
throw new Exception($"设备编号{param.SerialNumber}已存在");
var deviceType = "1";
var eai100Flag = param.Eai100Flag ?? false;
var updateTime = DateTime.Now;
var updateBy = UserContext.Current.UserInfo.UserName;
await _dbContext.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE XW_Device
SET DeviceType = {deviceType},
DeviceSerial = {param.SerialNumber},
CameraIp = {param.CameraIp},
Eai100Flag = {eai100Flag},
OrgId = {school.Id},
OrgName = {school.SchoolName},
Remark = {param.TenantName},
UpdateTime = {updateTime},
UpdateBy = {updateBy}
WHERE Id = {id}");
}
private async Task DeleteAiDevice(long deviceId)
{
var id = ToIntId(deviceId);
var device = await _dbContext.Set<Ai_Device>()
.AsTracking()
.FirstOrDefaultAsync(x => x.Id == id);
if (device == null)
throw new Exception("一体机设备不存在");
await EnsureAiDevicePermission(device.Code);
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
var associations = await _dbContext.Set<Ai_DeviceAssocSchool>()
.AsTracking()
.Where(x => x.Code == device.Code)
.ToListAsync();
_dbContext.Set<Ai_DeviceAssocSchool>().RemoveRange(associations);
_dbContext.Set<Ai_Device>().Remove(device);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
private async Task DeleteYiLuDevice(long deviceId)
{
var device = await _dbContext.Set<YL_Device>()
.AsTracking()
.FirstOrDefaultAsync(x => x.Id == deviceId);
if (device == null)
throw new Exception("壹路设备不存在");
await EnsureOrgPermission(device.OrgId);
_dbContext.Set<YL_Device>().Remove(device);
await _dbContext.SaveChangesAsync();
}
private async Task DeleteXinWeiDevice(long deviceId)
{
var id = ToIntId(deviceId);
var device = await _dbContext.Set<XW_Device>()
.AsNoTracking()
.Where(x => x.Id == id)
.Select(x => new
{
x.Id,
x.OrgId
})
.FirstOrDefaultAsync();
if (device == null)
throw new Exception("芯未设备不存在");
await EnsureOrgPermission(device.OrgId);
await _dbContext.Database.ExecuteSqlInterpolatedAsync(
$"DELETE FROM XW_Device WHERE Id = {id}");
}
private async Task EnsureAiDevicePermission(string serialNumber)
{
if (UserContext.Current.IsSuperAdmin)
return;
var tenantId = UserContext.Current.TenantId;
var allowed = await _dbContext.Set<Ai_DeviceAssocSchool>()
.AnyAsync(x => x.Code == serialNumber && x.SchoolCode == tenantId);
if (!allowed)
throw new Exception("当前账号无权操作该设备");
}
private async Task EnsureOrgPermission(int? orgId)
{
if (UserContext.Current.IsSuperAdmin)
return;
if (!orgId.HasValue || !await GetAllowedSchools().AnyAsync(x => x.Id == orgId.Value))
throw new Exception("当前账号无权操作该设备");
}
private static void NormalizeAndValidate(SaveDeviceParam param)
{
if (param == null)
throw new Exception("设备参数不能为空");
EnsureSourceType(param.SourceType);
param.OrgName = param.OrgName?.Trim();
param.SerialNumber = param.SerialNumber?.Trim();
param.TenantName = param.TenantName?.Trim();
param.CameraIp = param.CameraIp?.Trim();
if (param.OrgId <= 0)
throw new Exception("请选择学校");
if (string.IsNullOrWhiteSpace(param.OrgName))
throw new Exception("学校名称不能为空");
if (string.IsNullOrWhiteSpace(param.SerialNumber))
throw new Exception("设备编号不能为空");
if (string.IsNullOrWhiteSpace(param.TenantName))
throw new Exception("设备名称不能为空");
if (param.SourceType == DeviceSourceType.YiLuDevice && param.SerialNumber.Length > 64)
throw new Exception("壹路设备编号不能超过64个字符");
if (param.SourceType != DeviceSourceType.XinWeiDevice)
{
param.CameraIp = null;
param.Eai100Flag = null;
}
}
private static void EnsureSourceType(DeviceSourceType? sourceType)
{
if (sourceType.HasValue && !Enum.IsDefined(typeof(DeviceSourceType), sourceType.Value))
throw new Exception("设备类型无效");
}
private static int ToIntId(long id)
{
if (id <= 0 || id > int.MaxValue)
throw new Exception("设备Id无效");
return (int)id;
}
}
}