diff --git a/lib/utils/time.ts b/lib/utils/time.ts index b779029..04bbc8d 100644 --- a/lib/utils/time.ts +++ b/lib/utils/time.ts @@ -1,3 +1,35 @@ +import type { InjectionKey } from "vue"; + export function nowIso() { return new Date().toISOString(); } + +export const SITE_TIMEZONE_KEY: InjectionKey = Symbol("siteTimezone"); + +export function formatDateInTimezone(value: string | Date, timezone: string): string { + return new Date(value).toLocaleString("zh-CN", { timeZone: timezone }); +} + +function parseDateInTimezone(date: string | Date, timezone: string) { + const parts = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + }).formatToParts(new Date(date)); + return { + year: Number(parts.find((p) => p.type === "year")?.value), + month: Number(parts.find((p) => p.type === "month")?.value) - 1, + day: Number(parts.find((p) => p.type === "day")?.value), + }; +} + +export function getStartOfDay(date: string | Date, timezone: string): Date { + const { year, month, day } = parseDateInTimezone(date, timezone); + return new Date(Date.UTC(year, month, day)); +} + +export function getEndOfDay(date: string | Date, timezone: string): Date { + const { year, month, day } = parseDateInTimezone(date, timezone); + return new Date(Date.UTC(year, month, day, 23, 59, 59, 999)); +} diff --git a/modules/email/service.ts b/modules/email/service.ts index da95d92..08fcffc 100644 --- a/modules/email/service.ts +++ b/modules/email/service.ts @@ -786,7 +786,7 @@ export async function sendTestEmail(input: { toEmail: validated.toEmail, values: { siteName: site.siteName, - sentAt: new Date().toLocaleString("zh-CN"), + sentAt: new Date().toLocaleString("zh-CN", { timeZone: site.timezone || "Asia/Shanghai" }), customContent: validated.customContent, }, triggeredBy: `admin:${adminId || 0}`, diff --git a/modules/inventory/repository.ts b/modules/inventory/repository.ts index 911a5b3..1d39d01 100644 --- a/modules/inventory/repository.ts +++ b/modules/inventory/repository.ts @@ -1,4 +1,5 @@ import type { PrismaClient, Prisma, CardStatus } from "../../generated/prisma/client"; +import { getEndOfDay, getStartOfDay } from "../../lib/utils/time"; export function listCardRecords(prisma: PrismaClient) { return prisma.card.findMany({ @@ -74,6 +75,7 @@ export function listCardRecordsPaged( endDate?: string; page: number; pageSize: number; + timezone?: string; }, ) { const where: Prisma.CardWhereInput = {}; @@ -82,11 +84,9 @@ export function listCardRecordsPaged( if (params.status) where.status = params.status as CardStatus; if (params.startDate || params.endDate) { where.createdAt = {}; - if (params.startDate) where.createdAt.gte = new Date(params.startDate); + if (params.startDate) where.createdAt.gte = getStartOfDay(params.startDate, params.timezone || "Asia/Shanghai"); if (params.endDate) { - const end = new Date(params.endDate); - end.setHours(23, 59, 59, 999); - where.createdAt.lte = end; + where.createdAt.lte = getEndOfDay(params.endDate, params.timezone || "Asia/Shanghai"); } } const skip = (params.page - 1) * params.pageSize; diff --git a/modules/inventory/service.ts b/modules/inventory/service.ts index 0bdd373..c39d7b8 100644 --- a/modules/inventory/service.ts +++ b/modules/inventory/service.ts @@ -2,6 +2,7 @@ import { getContext } from "telefunc"; import type { PrismaClient } from "../../generated/prisma/client"; import { badRequestError } from "../../lib/app-error"; import { getAdminContext, logAdminOperation } from "../auth/service"; +import { getSiteSetting } from "../site/service"; import { parseCardLines } from "./importer"; import { countCardStats, createCardRecord, createManyCards, deleteCardById, deleteUnusedCardsByProduct, listCardRecords, listCardRecordsPaged, findExistingCardContents } from "./repository"; @@ -233,7 +234,8 @@ export async function getAdminCardsPaged(params: { pageSize: number; }) { const { prisma } = getAdminContext(); - const [cards, total] = await listCardRecordsPaged(prisma, params); + const site = await getSiteSetting(prisma); + const [cards, total] = await listCardRecordsPaged(prisma, { ...params, timezone: site.timezone || "Asia/Shanghai" }); return { total, items: cards.map((item) => ({ diff --git a/modules/order/service.ts b/modules/order/service.ts index 9d6496a..9e98f94 100644 --- a/modules/order/service.ts +++ b/modules/order/service.ts @@ -11,6 +11,8 @@ import { closeOrderRecord, createOrderRecord, findOrderById, findOrderWithProduc import { generateOrderNo, generateQueryToken } from "./number"; import { logger } from "../../lib/logger"; import { validateDiscountCode, calculateDiscount, applyDiscountCode } from "../discount/service"; +import { getSiteSetting } from "../site/service"; +import { getStartOfDay as getStartOfDayUtil } from "../../lib/utils/time"; function getOrderContext() { return getContext<{ prisma: PrismaClient }>(); @@ -399,8 +401,8 @@ export async function closeOrder(orderId: number) { export async function getDashboardMetrics(prisma?: PrismaClient) { const client = prisma ?? getOrderContext().prisma; - const today = new Date(); - today.setHours(0, 0, 0, 0); + const siteSetting = await getSiteSetting(client); + const today = getStartOfDayUtil(new Date(), siteSetting.timezone!); const [todayOrders, paidTodayOrders, productCount, availableCards] = await Promise.all([ client.order.count({ diff --git a/modules/site/repository.ts b/modules/site/repository.ts index eb9244c..125a788 100644 --- a/modules/site/repository.ts +++ b/modules/site/repository.ts @@ -20,6 +20,7 @@ export function upsertSiteSettingRecord( orderNotice?: string | null; headCode?: string | null; footerCode?: string | null; + timezone?: string | null; }, ) { return prisma.siteSetting.upsert({ @@ -37,6 +38,7 @@ export function upsertSiteSettingRecord( orderNotice: input.orderNotice ?? null, headCode: input.headCode ?? null, footerCode: input.footerCode ?? null, + timezone: input.timezone ?? null, }, update: { siteName: input.siteName, @@ -50,6 +52,7 @@ export function upsertSiteSettingRecord( orderNotice: input.orderNotice ?? null, headCode: input.headCode ?? null, footerCode: input.footerCode ?? null, + timezone: input.timezone ?? null, }, }); } diff --git a/modules/site/service.ts b/modules/site/service.ts index f5557de..3248215 100644 --- a/modules/site/service.ts +++ b/modules/site/service.ts @@ -17,6 +17,7 @@ const defaultSiteSetting = { orderNotice: null, headCode: null, footerCode: null, + timezone: "Asia/Shanghai", }; function normalizeSetting(record: Awaited>) { @@ -36,6 +37,7 @@ function normalizeSetting(record: Awaited