diff --git a/VOL.Business/IServices/SunshineRun/ISunshineRunService.cs b/VOL.Business/IServices/SunshineRun/ISunshineRunService.cs new file mode 100644 index 0000000..8f580b5 --- /dev/null +++ b/VOL.Business/IServices/SunshineRun/ISunshineRunService.cs @@ -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> GetPageList(SunshineRunPageParam param); + + Task> GetExportList(SunshineRunFilterParam param); + } +} diff --git a/VOL.Business/Services/SunshineRun/SunshineRunService.cs b/VOL.Business/Services/SunshineRun/SunshineRunService.cs new file mode 100644 index 0000000..4f4187c --- /dev/null +++ b/VOL.Business/Services/SunshineRun/SunshineRunService.cs @@ -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> 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 + { + Total = total, + TotalPages = (int)Math.Ceiling(total / (double)param.PageSize), + PageIndex = param.PageIndex, + PageSize = param.PageSize, + Datas = records + }; + } + + public async Task> 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 GetSchoolId() + { + if (UserContext.Current.IsSuperAdmin) + return null; + + var schoolId = await _dbContext.Set().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 { "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 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> 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(); + 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 + "秒"; + } + } +} diff --git a/VOL.Model/SunshineRun/SunshineRunModels.cs b/VOL.Model/SunshineRun/SunshineRunModels.cs new file mode 100644 index 0000000..edf27fb --- /dev/null +++ b/VOL.Model/SunshineRun/SunshineRunModels.cs @@ -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; } + } +} diff --git a/VOL.WebApi/Controllers/Business/SunshineRunController.cs b/VOL.WebApi/Controllers/Business/SunshineRunController.cs new file mode 100644 index 0000000..4f57811 --- /dev/null +++ b/VOL.WebApi/Controllers/Business/SunshineRunController.cs @@ -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 +{ + /// + /// AI阳光跑运动记录。 + /// + [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> GetPageList([FromQuery] SunshineRunPageParam param) + => _service.GetPageList(param); + + [HttpGet(nameof(Export))] + [ApiActionPermission("XW_SunshineRunTrain", ActionPermissionOptions.Search)] + public async Task Export([FromQuery] SunshineRunFilterParam param) + { + var rows = await _service.GetExportList(param); + var columns = new List + { + 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> { ["AI阳光跑"] = rows }, + columns, autoFitColumns: false); + return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"AI阳光跑{DateTime.Now:yyyyMMddHHmmss}.xlsx"); + } + } +} diff --git a/sunshine-run-menu.sql b/sunshine-run-menu.sql new file mode 100644 index 0000000..ca33c6c --- /dev/null +++ b/sunshine-run-menu.sql @@ -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;