【发布时间】:2021-03-17 16:46:58
【问题描述】:
更新:我已在此处添加了修复程序,供任何人寻找其正常工作的从前到后示例。
我知道这有点重复,但我似乎一直在寻找部分答案,但没有一个完整的解决方案。我看到很多关于在后端以 UTC 存储日期的 cmets,我确实想这样做,但在过去几年中,对于如何/在何处处理翻译没有真正的整体解决方案。
我了解 JS Date() 对象存储时区信息,然后当您在其上使用 JSON.stringify() 时,它会将其交换为 UTC 时间。这会将时间提前 7 小时(对于我的语言环境),所以每次我保存日期时,它都会提前 7 小时。我该如何解决?我确实希望将日期作为 UTC 存储在数据库中,但我不确定我需要在哪里执行转换才能将其从 UTC 取回,并在返回浏览器时转换为我的语言环境时间。
我已尝试在下面包含整个代码路径,希望有人能指出我缺少的内容。
首先,我有两个 EFCore5 类,我用它们以数据优先的方法构建我的数据库。一个收入来源和一个时间表,这些对象之间是一对一的关系。
public class Schedule
{
[Key]
public int ScheduleID { get; set; }
[Required]
public ScheduleFrequency Frequency { get; set; }
[DataType(DataType.Date)]
public DateTime Occurrence_First { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_LastConfirmed { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_LastPlanned { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_Final { get; set; }
public bool IsAutoConfirm { get; set; }
[DataType(DataType.Date)]
public DateTime DateTime_Created { get; set; }
[DataType(DataType.Date)]
public DateTime? DateTime_Deactivated { get; set; }
public bool HasCustomTransactionTime { get; set; }
[System.Text.Json.Serialization.JsonIgnore]
public IncomeSource IncomeSource { get; set; }
}
public class IncomeSource
{
[Key]
public int IncomeSourceID { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal ExpectedAmount { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal TotalFromSource { get; set; }
public int? DefaultToAccountID { get; set; }
public int ScheduleID { get; set; }
public BudgetorAccount Account { get; set; }
public Schedule Schedule { get; set; }
public int AccountID { get; set; }
[ForeignKey("DefaultToAccountID")]
public BudgetorAccount DefaultToAccount { get; set; }
}
public class BudgetorDbContext : DbContext
{
public BudgetorDbContext(DbContextOptions<BudgetorDbContext> options) : base (options)
{
}
public DbSet<BudgetorAccount> Accounts { get; set; }
public DbSet<IncomeSource> IncomeSources { get; set; }
public DbSet<Schedule> Schedules { get; set; }
}
API 控制器调用一个服务来转换视图模型或从视图模型转换并处理所有 CRUD 操作。该服务利用 EFCore 自动保存相关对象的能力来保存附加到 IncomeSource 的 Schedule
public class IncomeSourceDetailVM : AccountDetailVM
{
public int IncomeSourceId { get; set; }
public decimal ExpectedAmount { get; set; }
public decimal TotalFromSource { get; set; }
public int? DefaultToAccountID { get; set; }
public Schedule Schedule { get; set; }
public IncomeSourceDetailVM() : base(AccountType.IncomeSource)
{
this.Schedule = new Schedule()
{
Occurrence_First = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day)
};
}
}
public class BudgetorDataService : IBudgetorService
{
public async Task<IncomeSourceDetailVM> GetIncomeSourceDetailVM(int id)
{
var vm = await getIncomeSourceData()
.Where(inc => inc.AccountID == id)
.Select(incSrc => new IncomeSourceDetailVM()
{
IncomeSourceId = incSrc.IncomeSourceID,
ExpectedAmount = incSrc.ExpectedAmount,
AccountId = incSrc.AccountID,
AccountName = incSrc.Account.Name,
DateTime_Created = incSrc.Account.DateTime_Created,
DateTime_Deactivated = incSrc.Account.DateTime_Deactivated,
DefaultToAccountID = incSrc.DefaultToAccountID,
Schedule = incSrc.Schedule,
Notes = incSrc.Account.Notes,
})
.FirstOrDefaultAsync();
return vm;
}
public async Task<IncomeSourceDetailVM> CreateIncomeSource(IncomeSourceDetailVM incomeSourceToAdd)
{
if (incomeSourceToAdd == null)
{
throw new ArgumentNullException(nameof(incomeSourceToAdd));
}
try
{
BudgetorAccount newAcct = new BudgetorAccount()
{
AccountType = AccountType.IncomeSource,
DateTime_Created = DateTime.Now,
Name = incomeSourceToAdd.AccountName,
Notes = incomeSourceToAdd.Notes,
IsSystem = false
};
await _context.Accounts.AddAsync(newAcct);
await _context.SaveChangesAsync();
incomeSourceToAdd.AccountId = newAcct.AccountID;
incomeSourceToAdd.DateTime_Created = newAcct.DateTime_Created;
IncomeSource newIncSrc = new IncomeSource()
{
AccountID = newAcct.AccountID,
DefaultToAccountID = incomeSourceToAdd.DefaultToAccountID,
ExpectedAmount = incomeSourceToAdd.ExpectedAmount,
Schedule = incomeSourceToAdd.Schedule
};
await _context.IncomeSources.AddAsync(newIncSrc);
await _context.SaveChangesAsync();
incomeSourceToAdd.IncomeSourceId = newIncSrc.IncomeSourceID;
}
catch(Exception err)
{
throw err;
}
return incomeSourceToAdd;
}
}
[ApiController]
public class IncSrcController : ControllerBase
{
private readonly IBudgetorService _budgetorService;
public IncSrcController(IBudgetorService budgetorService)
{
this._budgetorService = budgetorService;
}
[HttpPost]
[Consumes("application/json")]
public async Task<ActionResult<IncomeSourceDetailVM>> PostIncSrc([FromBody] IncomeSourceDetailVM incSrcToAdd)
{
IncomeSourceDetailVM createdIncSrc = incSrcToAdd.AccountId == 0
? await this._budgetorService.CreateIncomeSource(incSrcToAdd)
: await this._budgetorService.UpdateIncomeSource(incSrcToAdd);
return StatusCode(201, createdIncSrc);
}
[HttpGet("{id}")]
public async Task<ActionResult<IncSrcManagementVM>> GetAccount(int? id)
{
IncSrcManagementVM result = new IncSrcManagementVM()
{
Account = (id == 0 || !id.HasValue)
? new IncomeSourceDetailVM()
: await this._budgetorService.GetIncomeSourceDetailVM(id.Value),
ToAccounts = await this._budgetorService.GetIncSrcToAccounts()
};
if (result.Account.DefaultToAccountID.HasValue)
{
result.ToAccounts
.Find(s => s.AccountId == result.Account.DefaultToAccountID)
.IsDefault = true;
}
return result;
}
}
在前端,我在一个形状与上面 C# 中的 IncomeSourceDetailVM 视图模型完全相同的 JS 对象上调用 JSON.stringify() 以将对象发送回服务器。 (我在一篇文章中读到在 JSON.stringify() 调用上使用替换器来手动处理字符串转换并调用 .toLocalDateString() 但随后 API 控制器无法将其识别为日期并拒绝调用。另外我希望将其存储作为后端的 UTC,因此日期始终在视图中本地化。)
const saveIncomeSource = async (incSrcDetailVM) => {
const transportObj = JSON.stringify(incSrcDetailVM);
const { data } = await axios.post(INC_SRC_ROUTE_PATH, transportObj, headerConfig);
await getIncSrcListItemVMs();
}
当对象回到我的 React 应用程序的状态时,我使用一个效果来更新本地状态。作为其中的一部分,我将服务器发送的所有字符串日期转换为new Date()。此过程将这些 UTC 日期字符串解释为当地时间而不是 UTC。
useEffect(() => {
const { account, toAccounts} = incSrcEditorVM;
if ((account.schedule
&& !(account.schedule instanceof ScheduleBase))
) {
account.schedule = ScheduleBase.clone(account.schedule);
}
setIncSrc(account)
setToAccounts(toAccounts);
setLoading(false);
return () => {
setLoading(true);
setToAccounts([...blankIncSrc.toAccounts]);
}
}, [incSrcEditorVM]);
export default class ScheduleBase {
scheduleID = 0;
frequency = 0;
occurrence_First;
occurrence_LastConfirmed = null;
occurrence_LastPlanned = null;
occurrence_Final = null;
dateTime_Created = new Date();
dateTime_Deactivated = null;
hasCustomTransactionTime = false;
isAutoConfirm = false;
constructor() {
this.occurrence_First = new Date(Date.now());
this.dateTime_Created = new Date(Date.now());
}
static clone(schedBaseShapedObj) {
const clone = new ScheduleBase();
clone.frequency = schedBaseShapedObj.frequency;
clone.scheduleID = schedBaseShapedObj.scheduleID;
clone.occurrence_First = new Date(schedBaseShapedObj.occurrence_First);
clone.occurrence_LastConfirmed = populateDate(schedBaseShapedObj.occurrence_LastConfirmed);
clone.occurrence_LastPlanned = populateDate(schedBaseShapedObj.occurrence_LastPlanned);
clone.occurrence_Final = populateDate(schedBaseShapedObj.occurrence_Final);
clone.dateTime_Created = new Date(schedBaseShapedObj.dateTime_Created);
clone.dateTime_Deactivated = populateDate(schedBaseShapedObj.dateTime_Deactivated);
clone.hasCustomTransactionTime = schedBaseShapedObj.hasCustomTransactionTime;
clone.isAutoConfirm = schedBaseShapedObj.isAutoConfirm;
return clone;
}
}
// **Fix goes in here**
function populateDate(nullableDate) {
return (nullableDate === null)
? null
: new Date(nullableDate + "Z");
}
我觉得这必须是一个足够普遍的问题,我只是想念一个更直接的路径来做到这一点。
提前感谢所有花时间阅读本文的人。
【问题讨论】:
-
自 ECMAScript Date 对象发明语言以来,没有任何变化。解析和 toString* 方法的支持格式已经发生了一些变化,但仅此而已。它被严重忽视了。 Date 对象没有任何关联的时区信息,Date.prototype.getTimezoneOffset 使用来自主机系统的信息。与 Date 实例关联的时间值(与 ECMAScript 纪元的毫秒偏移量)本质上是 UTC,并且是其唯一的数据属性。
-
@RobG 感谢您对此的澄清,您是否也有解决我的问题的方法?
-
不,否则我会发布它。 :-)
标签: javascript c# date datetime .net-5