From fc6e909452b7302df24f1e45c156bb61135b8ba5 Mon Sep 17 00:00:00 2001 From: cct0831 Date: Sat, 21 Mar 2026 08:32:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core):=20migrate=20DateTime.Now/UtcNow?= =?UTF-8?q?=20to=20TimeProvider=20(Phase=202=20=E2=80=94=20VMs,=20Mvc,=20T?= =?UTF-8?q?okenService)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct DateTime.Now/UtcNow calls with Wtm.TimeProvider or injected TimeProvider across 9 files: - Core VMs: BaseBatchVM, BaseImportVM, BasePagedListVM, BaseTemplateVM - Mvc: _FrameworkController, FrameworkFilter, _AnalysisController, FileExtension - Auth: TokenService (constructor-injected TimeProvider) Key decisions: - TokenService: inject TimeProvider via optional ctor param (defaults to System) - FrameworkFilter: null-safe fallback (ctrl.Wtm?.TimeProvider ?? DateTime.Now) - BaseTemplateVM: null-safe (Wtm?.TimeProvider ?? TimeProvider.System) since Template instance may not have Wtm assigned - Use .UtcDateTime (not .DateTime) for UTC contexts to preserve DateTimeKind.Utc - Skip: .cshtml cache-bust, TypeExtension random data, codegen templates, property initializers, static BuildCsv, JWT LifetimeValidator Closes #706 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/WalkingTec.Mvvm.Core/BaseBatchVM.cs | 4 ++-- src/WalkingTec.Mvvm.Core/BaseImportVM.cs | 6 ++--- src/WalkingTec.Mvvm.Core/BasePagedListVM.cs | 8 +++---- src/WalkingTec.Mvvm.Core/BaseTemplateVM.cs | 3 ++- .../Auth/JwtAuth/TokenService.cs | 22 ++++++++++--------- .../Filters/FrameworkFilter.cs | 6 ++--- .../Helper/FileExtension.cs | 3 ++- .../_AnalysisController.cs | 4 ++-- .../_FrameworkController.cs | 13 ++++++----- 9 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/WalkingTec.Mvvm.Core/BaseBatchVM.cs b/src/WalkingTec.Mvvm.Core/BaseBatchVM.cs index e79ff4a1c..1ff1e97bc 100644 --- a/src/WalkingTec.Mvvm.Core/BaseBatchVM.cs +++ b/src/WalkingTec.Mvvm.Core/BaseBatchVM.cs @@ -151,7 +151,7 @@ public virtual bool DoBatchDelete() DC!.UpdateProperty(Entity, "IsValid"); if (isBasePoco) { - (Entity as IBasePoco)!.UpdateTime = DateTime.Now; + (Entity as IBasePoco)!.UpdateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; (Entity as IBasePoco)!.UpdateBy = LoginUserInfo?.ITCode; DC!.UpdateProperty(Entity, "UpdateTime"); DC!.UpdateProperty(Entity, "UpdateBy"); @@ -350,7 +350,7 @@ public virtual bool DoBatchEdit() IBasePoco ent = (entity as IBasePoco)!; if (ent.UpdateTime == null) { - ent.UpdateTime = DateTime.Now; + ent.UpdateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; DC!.UpdateProperty(entity, nameof(ent.UpdateTime)); } if (string.IsNullOrEmpty(ent.UpdateBy)) diff --git a/src/WalkingTec.Mvvm.Core/BaseImportVM.cs b/src/WalkingTec.Mvvm.Core/BaseImportVM.cs index cd11d470e..7764fd621 100644 --- a/src/WalkingTec.Mvvm.Core/BaseImportVM.cs +++ b/src/WalkingTec.Mvvm.Core/BaseImportVM.cs @@ -568,7 +568,7 @@ public virtual void SetEntityData() if (typeof(IBasePoco).IsAssignableFrom(SubTypeEntity.GetType())) { - (SubTypeEntity as IBasePoco)!.CreateTime = DateTime.Now; + (SubTypeEntity as IBasePoco)!.CreateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; (SubTypeEntity as IBasePoco)!.CreateBy = LoginUserInfo?.ITCode; } if (typeof(ITenant).IsAssignableFrom(SubTypeEntity.GetType())) @@ -1032,7 +1032,7 @@ public virtual bool BatchSaveData(IProgress? progress = null) { if (typeof(IBasePoco).IsAssignableFrom(exist.GetType())) { - (exist as IBasePoco)!.UpdateTime = DateTime.Now; + (exist as IBasePoco)!.UpdateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; DC!.UpdateProperty(exist, "UpdateTime"); } } @@ -1071,7 +1071,7 @@ public virtual bool BatchSaveData(IProgress? progress = null) //进行添加操作 if (typeof(IBasePoco).IsAssignableFrom(item.GetType())) { - (item as IBasePoco)!.CreateTime = DateTime.Now; + (item as IBasePoco)!.CreateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; (item as IBasePoco)!.CreateBy = LoginUserInfo?.ITCode; } if (typeof(ITenant).IsAssignableFrom(ModelType)) diff --git a/src/WalkingTec.Mvvm.Core/BasePagedListVM.cs b/src/WalkingTec.Mvvm.Core/BasePagedListVM.cs index 13d152ccd..af0b42073 100644 --- a/src/WalkingTec.Mvvm.Core/BasePagedListVM.cs +++ b/src/WalkingTec.Mvvm.Core/BasePagedListVM.cs @@ -174,7 +174,7 @@ public virtual byte[] GenerateExcel() else { Guid g = Guid.NewGuid(); - var FileName = typeof(TModel).Name + "_" + DateTime.Now.ToString("yyyyMMddHHmmssffff"); + var FileName = typeof(TModel).Name + "_" + Wtm!.TimeProvider.GetLocalNow().DateTime.ToString("yyyyMMddHHmmssffff"); //文件根目录 string RootPath = $"{Wtm!.ConfigInfo.HostRoot}\\export{g}"; @@ -1133,7 +1133,7 @@ public virtual void UpdateEntityList(bool updateAllFields = false) IBasePoco ent = (IBasePoco)newitem; if (ent.UpdateTime == null) { - ent.UpdateTime = DateTime.Now; + ent.UpdateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; } if (string.IsNullOrEmpty(ent.UpdateBy)) { @@ -1211,7 +1211,7 @@ public virtual void UpdateEntityList(bool updateAllFields = false) (item as IPersistPoco)!.IsValid = false; if (typeof(IBasePoco).IsAssignableFrom(ftype)) { - (item as IBasePoco)!.UpdateTime = DateTime.Now; + (item as IBasePoco)!.UpdateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; (item as IBasePoco)!.UpdateBy = LoginUserInfo?.ITCode; } dynamic i = item; @@ -1238,7 +1238,7 @@ public virtual void UpdateEntityList(bool updateAllFields = false) IBasePoco ent = (IBasePoco)item; if (ent.CreateTime == null) { - ent.CreateTime = DateTime.Now; + ent.CreateTime = Wtm!.TimeProvider.GetLocalNow().DateTime; } if (string.IsNullOrEmpty(ent.CreateBy)) { diff --git a/src/WalkingTec.Mvvm.Core/BaseTemplateVM.cs b/src/WalkingTec.Mvvm.Core/BaseTemplateVM.cs index a5baa21d2..f54d25c3b 100644 --- a/src/WalkingTec.Mvvm.Core/BaseTemplateVM.cs +++ b/src/WalkingTec.Mvvm.Core/BaseTemplateVM.cs @@ -107,7 +107,8 @@ public byte[] GenerateTemplate(out string displayName) { //设置导出的文件名称 string SheetName = !string.IsNullOrEmpty(FileDisplayName) ? FileDisplayName : this.GetType().Name; - displayName = SheetName + "_" + DateTime.Now.ToString("yyyy-MM-dd") + "_" + DateTime.Now.ToString("hh^mm^ss") + ".xlsx"; + var now = (Wtm?.TimeProvider ?? TimeProvider.System).GetLocalNow().DateTime; + displayName = SheetName + "_" + now.ToString("yyyy-MM-dd") + "_" + now.ToString("hh^mm^ss") + ".xlsx"; //1.声明Excel文档 IWorkbook workbook = new XSSFWorkbook(); diff --git a/src/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs b/src/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs index b90892dad..69bff26b4 100644 --- a/src/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs +++ b/src/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs @@ -20,12 +20,14 @@ public class TokenService : ITokenService { private readonly JwtOption _jwtOptions; private readonly IServiceProvider _sp; + private readonly TimeProvider _timeProvider; private const int RefreshTokenExpiryDays = 7; - public TokenService(IOptionsMonitor configs, IServiceProvider sp) + public TokenService(IOptionsMonitor configs, IServiceProvider sp, TimeProvider? timeProvider = null) { _jwtOptions = configs.CurrentValue.JwtOptions; _sp = sp; + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task IssueTokenAsync( @@ -61,13 +63,13 @@ public async Task RefreshTokenAsync( if (existing is { IsRevoked: true, ReplacedByToken: not null }) { await RevokeDescendantsAsync(dbSet, existing, ipAddress, - "Attempted reuse of revoked token"); + "Attempted reuse of revoked token", _timeProvider); await dc.SaveChangesAsync(); } return null; } var newTokenString = GenerateRefreshTokenString(); - existing.RevokedUtc = DateTime.UtcNow; + existing.RevokedUtc = _timeProvider.GetUtcNow().UtcDateTime; existing.RevokedByIp = ipAddress; existing.ReplacedByToken = newTokenString; existing.RevokeReason = "Rotated"; @@ -76,7 +78,7 @@ await RevokeDescendantsAsync(dbSet, existing, ipAddress, Token = newTokenString, ITCode = existing.ITCode, TenantCode = existing.TenantCode, - ExpiresUtc = DateTime.UtcNow.AddDays(RefreshTokenExpiryDays), + ExpiresUtc = _timeProvider.GetUtcNow().UtcDateTime.AddDays(RefreshTokenExpiryDays), CreatedByIp = ipAddress }; await dbSet.AddAsync(newEntity); @@ -102,7 +104,7 @@ public async Task RevokeTokenAsync( var existing = await dc.Set() .FirstOrDefaultAsync(x => x.Token == refreshToken); if (existing == null || !existing.IsActive) return; - existing.RevokedUtc = DateTime.UtcNow; + existing.RevokedUtc = _timeProvider.GetUtcNow().UtcDateTime; existing.RevokedByIp = ipAddress; existing.RevokeReason = reason ?? "Explicit revocation"; await dc.SaveChangesAsync(); @@ -128,7 +130,7 @@ private string GenerateAccessToken(LoginUserInfo info) issuer: _jwtOptions.Issuer, audience: _jwtOptions.Audience, claims: claims, - expires: DateTime.UtcNow.AddSeconds(_jwtOptions.Expires), + expires: _timeProvider.GetUtcNow().UtcDateTime.AddSeconds(_jwtOptions.Expires), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(jwt); } @@ -142,7 +144,7 @@ private async Task CreateRefreshTokenAsync( { Token = GenerateRefreshTokenString(), ITCode = itCode, TenantCode = tenantCode, - ExpiresUtc = DateTime.UtcNow.AddDays(RefreshTokenExpiryDays), + ExpiresUtc = _timeProvider.GetUtcNow().UtcDateTime.AddDays(RefreshTokenExpiryDays), CreatedByIp = ipAddress }; if (dc != null) @@ -161,7 +163,7 @@ private static string GenerateRefreshTokenString() private static async Task RevokeDescendantsAsync( DbSet dbSet, RefreshTokenEntity token, - string ipAddress, string reason) + string ipAddress, string reason, TimeProvider timeProvider) { if (string.IsNullOrEmpty(token.ReplacedByToken)) return; var child = await dbSet.FirstOrDefaultAsync( @@ -169,13 +171,13 @@ private static async Task RevokeDescendantsAsync( if (child == null) return; if (child.IsActive) { - child.RevokedUtc = DateTime.UtcNow; + child.RevokedUtc = timeProvider.GetUtcNow().UtcDateTime; child.RevokedByIp = ipAddress; child.RevokeReason = reason; } else { - await RevokeDescendantsAsync(dbSet, child, ipAddress, reason); + await RevokeDescendantsAsync(dbSet, child, ipAddress, reason, timeProvider); } } } diff --git a/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs b/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs index 3266c5835..be702fff4 100644 --- a/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs +++ b/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs @@ -35,7 +35,7 @@ public override void OnActionExecuting(ActionExecutingContext context) context.SetWtmContext(); if (context.HttpContext.Items.ContainsKey("actionstarttime") == false) { - context.HttpContext.Items.Add("actionstarttime", DateTime.Now); + context.HttpContext.Items.Add("actionstarttime", ctrl.Wtm.TimeProvider.GetLocalNow().DateTime); } var ctrlActDesc = context.ActionDescriptor as ControllerActionDescriptor; var log = new SimpleLog();// 初始化log备用 @@ -377,7 +377,7 @@ public override void OnResultExecuted(ResultExecutedContext context) var postDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(HttpPostAttribute), false).Cast().FirstOrDefault(); log.LogType = context.Exception == null ? ActionLogTypesEnum.Normal : ActionLogTypesEnum.Exception; - log.ActionTime = DateTime.Now; + log.ActionTime = ctrl.Wtm?.TimeProvider.GetLocalNow().DateTime ?? DateTime.Now; log.ITCode = ctrl.Wtm?.LoginUserInfo?.ITCode ?? string.Empty; // 给日志的多语言属性赋值 log.ModuleName = ctrlDes?.GetDescription(ctrl) ?? ctrlActDesc.ControllerName; @@ -392,7 +392,7 @@ public override void OnResultExecuted(ResultExecutedContext context) var starttime = context.HttpContext.Items["actionstarttime"] as DateTime?; if (starttime != null) { - log.Duration = DateTime.Now.Subtract(starttime.Value).TotalSeconds; + log.Duration = (ctrl.Wtm?.TimeProvider.GetLocalNow().DateTime ?? DateTime.Now).Subtract(starttime.Value).TotalSeconds; } try { diff --git a/src/WalkingTec.Mvvm.Mvc/Helper/FileExtension.cs b/src/WalkingTec.Mvvm.Mvc/Helper/FileExtension.cs index 1450a353e..9cd71f30e 100644 --- a/src/WalkingTec.Mvvm.Mvc/Helper/FileExtension.cs +++ b/src/WalkingTec.Mvvm.Mvc/Helper/FileExtension.cs @@ -22,7 +22,8 @@ public static class FileExtension var data = self.GenerateExcel(); string ContentType = self.ExportExcelCount > 1 ? "application/x-zip-compresse" : "application/vnd.ms-excel"; ExportName = string.IsNullOrEmpty(ExportName) ? typeof(T).Name : ExportName; - ExportName = self.ExportExcelCount > 1 ? $"Export_{ExportName}_{DateTime.Now.ToString("yyyyMMddHHmmssffff")}.zip" : $"Export_{ExportName}_{DateTime.Now.ToString("yyyyMMddHHmmssffff")}.xlsx"; + var now = self.Wtm!.TimeProvider.GetLocalNow().DateTime; + ExportName = self.ExportExcelCount > 1 ? $"Export_{ExportName}_{now.ToString("yyyyMMddHHmmssffff")}.zip" : $"Export_{ExportName}_{now.ToString("yyyyMMddHHmmssffff")}.xlsx"; FileContentResult Result = new FileContentResult(data, ContentType); Result.FileDownloadName = ExportName; return Result; diff --git a/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs b/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs index 0e111330e..586dd3e74 100644 --- a/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs +++ b/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs @@ -350,7 +350,7 @@ public IActionResult SaveQuery([FromBody] SaveQueryRequest? req) ConfigJson = configJson, OwnerCode = userCode, IsPublic = req.IsPublic, - CreateTime = DateTime.Now, + CreateTime = Wtm!.TimeProvider.GetLocalNow().DateTime, CreateBy = userCode }; @@ -442,7 +442,7 @@ private void WriteAnalysisActionLog( var log = new ActionLog { LogType = ActionLogTypesEnum.Normal, - ActionTime = DateTime.Now, + ActionTime = Wtm!.TimeProvider.GetLocalNow().DateTime, ITCode = Wtm?.LoginUserInfo?.ITCode ?? string.Empty, ModuleName = "Analysis", ActionName = actionName, diff --git a/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs b/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs index 6d6a209c6..722f68f81 100644 --- a/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs +++ b/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs @@ -253,9 +253,10 @@ public IActionResult GetExportExcel(string _DONOT_USE_VMNAME, string _DONOT_USE_ return StatusCode(422, new { message = MvcProgram._localizer?["Sys.NoData"] ?? "No data" }); } - HttpContext.Response.Cookies.Append("DONOTUSEDOWNLOADING", "0", new Microsoft.AspNetCore.Http.CookieOptions() { Path = "/", Expires = DateTime.Now.AddDays(2) }); + var now = Wtm.TimeProvider.GetLocalNow().DateTime; + HttpContext.Response.Cookies.Append("DONOTUSEDOWNLOADING", "0", new Microsoft.AspNetCore.Http.CookieOptions() { Path = "/", Expires = now.AddDays(2) }); - return File(data, "application/vnd.ms-excel", $"Export_{instanceType.Name}_{DateTime.Now.ToString("yyyy-MM-dd")}.xls"); + return File(data, "application/vnd.ms-excel", $"Export_{instanceType.Name}_{now.ToString("yyyy-MM-dd")}.xls"); } else { @@ -280,7 +281,7 @@ public IActionResult GetExcelTemplate(string _DONOT_USE_VMNAME, string _DONOT_US } importVM.SetParms(qs); var data = importVM.GenerateTemplate(out string fileName); - HttpContext.Response.Cookies.Append("DONOTUSEDOWNLOADING", "0", new Microsoft.AspNetCore.Http.CookieOptions() { Domain = "/", Expires = DateTime.Now.AddDays(2) }); + HttpContext.Response.Cookies.Append("DONOTUSEDOWNLOADING", "0", new Microsoft.AspNetCore.Http.CookieOptions() { Domain = "/", Expires = Wtm.TimeProvider.GetLocalNow().DateTime.AddDays(2) }); return File(data, "application/vnd.ms-excel", fileName); } @@ -293,7 +294,7 @@ public IActionResult Error() var ex = HttpContext.Features.Get(); ActionLog log = new ActionLog(); log.LogType = ActionLogTypesEnum.Exception; - log.ActionTime = DateTime.Now; + log.ActionTime = Wtm.TimeProvider.GetLocalNow().DateTime; log.ITCode = Wtm.LoginUserInfo?.ITCode ?? string.Empty; var controllerDes = ex.Error.TargetSite.DeclaringType.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); @@ -316,7 +317,7 @@ public IActionResult Error() DateTime? starttime = HttpContext.Items["actionstarttime"] as DateTime?; if (starttime != null) { - log.Duration = DateTime.Now.Subtract(starttime.Value).TotalSeconds; + log.Duration = Wtm.TimeProvider.GetLocalNow().DateTime.Subtract(starttime.Value).TotalSeconds; } var logger = HttpContext.RequestServices.GetRequiredService>(); if (logger != null) @@ -655,7 +656,7 @@ public IActionResult UploadForLayUIUEditor([FromServices] WtmFileProvider fp, st //通过Base64方式上传附件 var FileData = Convert.FromBase64String(Request.Form["FileID"]); MemoryStream MS = new MemoryStream(FileData); - file = fp.Upload("SCRAWL_" + DateTime.Now.ToString("yyyyMMddHHmmssttt") + ".jpg", FileData.Length, MS, groupName, subdir, dc: Wtm.CreateDC(cskey: _DONOT_USE_CS)); + file = fp.Upload("SCRAWL_" + Wtm.TimeProvider.GetLocalNow().DateTime.ToString("yyyyMMddHHmmssttt") + ".jpg", FileData.Length, MS, groupName, subdir, dc: Wtm.CreateDC(cskey: _DONOT_USE_CS)); MS.Dispose(); }