feat: 添加AI阳光跑数据接口与班级筛选

从 XW_SunshineRunTrain 按学校权限查询运动记录,支持分页、姓名、日期和年级班级 ID 精确筛选。

提供 Excel 导出并附带学校角色的菜单授权脚本。
This commit is contained in:
ltx 2026-10-10 10:06:33 +08:00
parent e1fac41f8c
commit cd7206ede5
5 changed files with 377 additions and 0 deletions

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using VOL.Model;
using VOL.Model.SunshineRun;
namespace VOL.Business.IServices.SunshineRun
{
public interface ISunshineRunService
{
Task<PageDataDto<SunshineRunRecordModel>> GetPageList(SunshineRunPageParam param);
Task<List<SunshineRunRecordModel>> GetExportList(SunshineRunFilterParam param);
}
}

View File

@ -0,0 +1,212 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using VOL.Business.IServices.SunshineRun;
using VOL.Core.EFDbContext;
using VOL.Core.Extensions.AutofacManager;
using VOL.Core.ManageUser;
using VOL.Entity.DomainModels;
using VOL.Model;
using VOL.Model.SunshineRun;
namespace VOL.Business.Services.SunshineRun
{
public class SunshineRunService : ISunshineRunService, IDependency
{
private readonly VOLContext _dbContext;
public SunshineRunService(VOLContext dbContext)
{
_dbContext = dbContext;
}
public async Task<PageDataDto<SunshineRunRecordModel>> GetPageList(SunshineRunPageParam param)
{
param ??= new SunshineRunPageParam();
ValidateDateRange(param);
var schoolId = await GetSchoolId();
var total = await GetCount(param, schoolId);
var records = await GetRecords(param, schoolId, param.PageIndex, param.PageSize);
return new PageDataDto<SunshineRunRecordModel>
{
Total = total,
TotalPages = (int)Math.Ceiling(total / (double)param.PageSize),
PageIndex = param.PageIndex,
PageSize = param.PageSize,
Datas = records
};
}
public async Task<List<SunshineRunRecordModel>> GetExportList(SunshineRunFilterParam param)
{
param ??= new SunshineRunFilterParam();
ValidateDateRange(param);
return await GetRecords(param, await GetSchoolId(), null, null);
}
private static void ValidateDateRange(SunshineRunFilterParam param)
{
if (param.StartDate.HasValue && param.EndDate.HasValue &&
param.StartDate.Value.Date > param.EndDate.Value.Date)
throw new ArgumentException("结束日期不能早于开始日期");
}
private async Task<string> GetSchoolId()
{
if (UserContext.Current.IsSuperAdmin)
return null;
var schoolId = await _dbContext.Set<S_School>().AsNoTracking()
.Where(x => x.SchoolCode == UserContext.Current.TenantId)
.Select(x => (int?)x.Id)
.FirstOrDefaultAsync();
return schoolId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
}
private static string BuildWhere(SunshineRunFilterParam param, string schoolId)
{
var conditions = new List<string> { "1 = 1" };
if (schoolId != null)
conditions.Add("r.OrgId = @OrgId");
if (!string.IsNullOrWhiteSpace(param.StudentName))
conditions.Add("r.StudentName LIKE @StudentName");
if (!string.IsNullOrWhiteSpace(param.ClassName))
conditions.Add("(r.ClassName LIKE @ClassName OR CONCAT(r.GradeName, r.ClassName) LIKE @ClassName)");
if (param.GradeId.HasValue)
conditions.Add("r.GradeId = @GradeId");
if (param.ClassId.HasValue)
conditions.Add("r.ClassId = @ClassId");
// CreateTime is stored as an ISO-formatted string in XW_SunshineRunTrain.
if (param.StartDate.HasValue)
conditions.Add("r.CreateTime >= @StartDate");
if (param.EndDate.HasValue)
conditions.Add("r.CreateTime < @EndDateExclusive");
return string.Join(" AND ", conditions);
}
private static void AddParameter(DbCommand command, string name, object value)
{
var parameter = command.CreateParameter();
parameter.ParameterName = name;
parameter.Value = value ?? DBNull.Value;
command.Parameters.Add(parameter);
}
private static void AddFilters(DbCommand command, SunshineRunFilterParam param, string schoolId)
{
if (schoolId != null)
AddParameter(command, "@OrgId", schoolId);
if (!string.IsNullOrWhiteSpace(param.StudentName))
AddParameter(command, "@StudentName", "%" + param.StudentName.Trim() + "%");
if (!string.IsNullOrWhiteSpace(param.ClassName))
AddParameter(command, "@ClassName", "%" + param.ClassName.Trim() + "%");
if (param.GradeId.HasValue)
AddParameter(command, "@GradeId", param.GradeId.Value.ToString(CultureInfo.InvariantCulture));
if (param.ClassId.HasValue)
AddParameter(command, "@ClassId", param.ClassId.Value.ToString(CultureInfo.InvariantCulture));
if (param.StartDate.HasValue)
AddParameter(command, "@StartDate",
param.StartDate.Value.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
if (param.EndDate.HasValue)
AddParameter(command, "@EndDateExclusive",
param.EndDate.Value.Date.AddDays(1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
private async Task<int> GetCount(SunshineRunFilterParam param, string schoolId)
{
var connection = _dbContext.Database.GetDbConnection();
await connection.OpenAsync();
try
{
await using var command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM XW_SunshineRunTrain r WHERE " + BuildWhere(param, schoolId);
AddFilters(command, param, schoolId);
return Convert.ToInt32(await command.ExecuteScalarAsync(), CultureInfo.InvariantCulture);
}
finally
{
await connection.CloseAsync();
}
}
private async Task<List<SunshineRunRecordModel>> GetRecords(
SunshineRunFilterParam param, string schoolId, int? pageIndex, int? pageSize)
{
var connection = _dbContext.Database.GetDbConnection();
await connection.OpenAsync();
try
{
await using var command = connection.CreateCommand();
command.CommandText = @"SELECT r.StudentName, r.Sex, r.GradeName, r.ClassName,
r.EffectiveDistance, r.InvalidDistance, r.Times, r.InvalidTimes, r.CreateTime
FROM XW_SunshineRunTrain r WHERE " + BuildWhere(param, schoolId) +
" ORDER BY r.CreateTime DESC, r.Id DESC" +
(pageIndex.HasValue ? " OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY" : "");
AddFilters(command, param, schoolId);
if (pageIndex.HasValue)
{
AddParameter(command, "@Offset", (pageIndex.Value - 1) * pageSize.Value);
AddParameter(command, "@PageSize", pageSize.Value);
}
var rows = new List<SunshineRunRecordModel>();
await using var reader = await command.ExecuteReaderAsync();
var index = pageIndex.HasValue ? (pageIndex.Value - 1) * pageSize.Value : 0;
while (await reader.ReadAsync())
{
var effectiveDistance = GetInt(reader, 4);
var invalidDistance = GetInt(reader, 5);
var seconds = GetInt(reader, 6);
var invalidSeconds = GetInt(reader, 7);
var start = reader.IsDBNull(8) ? null : reader.GetString(8);
var parsedStart = DateTime.TryParse(start, CultureInfo.InvariantCulture,
DateTimeStyles.None, out var startTime);
rows.Add(new SunshineRunRecordModel
{
Index = ++index,
StudentName = GetString(reader, 0),
SexName = GetInt(reader, 1) switch { 1 => "男", 2 => "女", _ => "-" },
ClassName = string.Join(" ", new[] { GetString(reader, 2), GetString(reader, 3) }
.Where(x => !string.IsNullOrWhiteSpace(x))),
EffectiveDistance = FormatDistance(effectiveDistance),
EffectiveTime = FormatTime(seconds.HasValue
? Math.Max(0, seconds.Value - (invalidSeconds ?? 0)) : null),
TotalDistance = FormatDistance(effectiveDistance.HasValue || invalidDistance.HasValue
? (effectiveDistance ?? 0) + (invalidDistance ?? 0) : null),
TotalTime = FormatTime(seconds),
StartTime = parsedStart ? startTime.ToString("yyyy-MM-dd HH:mm:ss") : start ?? "-",
EndTime = parsedStart && seconds.HasValue
? startTime.AddSeconds(seconds.Value).ToString("yyyy-MM-dd HH:mm:ss") : "-"
});
}
return rows;
}
finally
{
await connection.CloseAsync();
}
}
private static string GetString(DbDataReader reader, int ordinal)
=> reader.IsDBNull(ordinal) ? string.Empty : reader.GetString(ordinal);
private static int? GetInt(DbDataReader reader, int ordinal)
=> reader.IsDBNull(ordinal) ? null : Convert.ToInt32(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
private static string FormatDistance(int? meters)
=> meters.HasValue ? (meters.Value / 1000m).ToString("0.##", CultureInfo.InvariantCulture) + "公里" : "-";
private static string FormatTime(int? seconds)
{
if (!seconds.HasValue)
return "-";
var value = Math.Max(0, seconds.Value);
var minutes = value / 60;
return minutes > 0 ? minutes + "分" + value % 60 + "秒" : value + "秒";
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace VOL.Model.SunshineRun
{
public class SunshineRunFilterParam
{
public string StudentName { get; set; }
public string ClassName { get; set; }
public int? GradeId { get; set; }
public int? ClassId { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
public class SunshineRunPageParam : SunshineRunFilterParam
{
[Range(1, int.MaxValue)]
public int PageIndex { get; set; } = 1;
[Range(1, 200)]
public int PageSize { get; set; } = 10;
}
public class SunshineRunRecordModel
{
[Display(Name = "序号")]
public int Index { get; set; }
[Display(Name = "姓名")]
public string StudentName { get; set; }
[Display(Name = "性别")]
public string SexName { get; set; }
[Display(Name = "班级")]
public string ClassName { get; set; }
[Display(Name = "有效里程")]
public string EffectiveDistance { get; set; }
[Display(Name = "有效用时")]
public string EffectiveTime { get; set; }
[Display(Name = "总里程")]
public string TotalDistance { get; set; }
[Display(Name = "总用时")]
public string TotalTime { get; set; }
[Display(Name = "开始时间")]
public string StartTime { get; set; }
[Display(Name = "结束时间")]
public string EndTime { get; set; }
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using VOL.Business.IServices.SunshineRun;
using VOL.Core.Enums;
using VOL.Core.Filters;
using VOL.Core.Utilities;
using VOL.Model;
using VOL.Model.SunshineRun;
namespace VOL.WebApi.Controllers.Business
{
/// <summary>
/// AI阳光跑运动记录。
/// </summary>
[Route("api/[controller]")]
[ApiController]
[ApiExplorerSettings(GroupName = "v3")]
[TypeFilter(typeof(CustomApiResponseFilter))]
public class SunshineRunController : ControllerBase
{
private readonly ISunshineRunService _service;
public SunshineRunController(ISunshineRunService service)
{
_service = service;
}
[HttpGet(nameof(GetPageList))]
[ApiActionPermission("XW_SunshineRunTrain", ActionPermissionOptions.Search)]
public Task<PageDataDto<SunshineRunRecordModel>> GetPageList([FromQuery] SunshineRunPageParam param)
=> _service.GetPageList(param);
[HttpGet(nameof(Export))]
[ApiActionPermission("XW_SunshineRunTrain", ActionPermissionOptions.Search)]
public async Task<IActionResult> Export([FromQuery] SunshineRunFilterParam param)
{
var rows = await _service.GetExportList(param);
var columns = new List<string>
{
nameof(SunshineRunRecordModel.Index),
nameof(SunshineRunRecordModel.StudentName),
nameof(SunshineRunRecordModel.SexName),
nameof(SunshineRunRecordModel.ClassName),
nameof(SunshineRunRecordModel.EffectiveDistance),
nameof(SunshineRunRecordModel.EffectiveTime),
nameof(SunshineRunRecordModel.TotalDistance),
nameof(SunshineRunRecordModel.TotalTime),
nameof(SunshineRunRecordModel.StartTime),
nameof(SunshineRunRecordModel.EndTime)
};
var bytes = Tool.ExportToExcel(
new Dictionary<string, List<SunshineRunRecordModel>> { ["AI阳光跑"] = rows },
columns, autoFitColumns: false);
return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"AI阳光跑{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}
}
}

29
sunshine-run-menu.sql Normal file
View File

@ -0,0 +1,29 @@
SET XACT_ABORT ON;
BEGIN TRANSACTION;
DECLARE @parentId int = (SELECT TOP 1 Menu_Id FROM Sys_Menu WHERE MenuName = N'运动记录' AND ParentId = 0);
IF @parentId IS NULL
THROW 50001, N'未找到运动记录父菜单', 1;
IF NOT EXISTS (SELECT 1 FROM Sys_Role WHERE Role_Id = 2 AND RoleName = N'学校')
THROW 50002, N'未找到学校角色', 1;
IF NOT EXISTS (SELECT 1 FROM Sys_Menu WHERE Url = '/sunshineRun')
BEGIN
INSERT INTO Sys_Menu
(ParentId, MenuName, TableName, Url, Auth, Description, Icon,
OrderNo, Creator, CreateDate, Enable, MenuType)
VALUES
(@parentId, N'AI阳光跑', N'XW_SunshineRunTrain', '/sunshineRun',
N'[{"text":"查询","value":"Search"}]', N'芯未阳光跑运动记录',
'el-icon-s-data', 1, 'system', GETDATE(), 1, 0);
END;
DECLARE @menuId int = (SELECT TOP 1 Menu_Id FROM Sys_Menu WHERE Url = '/sunshineRun');
IF NOT EXISTS (SELECT 1 FROM Sys_RoleAuth WHERE Role_Id = 2 AND Menu_Id = @menuId)
BEGIN
INSERT INTO Sys_RoleAuth (Role_Id, Menu_Id, AuthValue, Creator, CreateDate)
VALUES (2, @menuId, 'Search', 'system', GETDATE());
END;
COMMIT TRANSACTION;