From 5edebd9d5559f9cfb6b7c0e842ec9cf33bba4904 Mon Sep 17 00:00:00 2001 From: dmy Date: Thu, 15 Jan 2026 16:17:41 +0800 Subject: [PATCH] refactor: improve date handling and timezone consistency - Add timezone support to database module (+08:00) - Extract date formatting utilities to shared modules - Standardize timezone handling across frontend and backend - Improve date formatting consistency in UI components - Refactor crawler page.goto options for better readability --- frontend/src/components/Bids.vue | 6 +- frontend/src/components/CrawlInfo.vue | 18 +--- frontend/src/components/Dashboard.vue | 6 +- frontend/src/components/PinnedProject.vue | 13 +-- frontend/src/utils/date.util.ts | 58 ++++++++++++ package.json | 2 +- src/bids/services/bid.service.ts | 18 ++-- src/common/logger/winston.config.ts | 6 +- src/common/utils/timezone.util.ts | 98 +++++++++++++++++++++ src/crawler/services/cdt_target.ts | 5 +- src/crawler/services/ceic_target.ts | 5 +- src/crawler/services/cgnpc_target.ts | 5 +- src/crawler/services/chdtp_target.ts | 5 +- src/crawler/services/cnncecp_target.ts | 5 +- src/crawler/services/cnooc_target.ts | 5 +- src/crawler/services/eps_target.ts | 5 +- src/crawler/services/powerbeijing_target.ts | 5 +- src/crawler/services/sdicc_target.ts | 5 +- src/crawler/services/szecp_target.ts | 5 +- src/database/database.module.ts | 1 + 20 files changed, 219 insertions(+), 57 deletions(-) create mode 100644 frontend/src/utils/date.util.ts create mode 100644 src/common/utils/timezone.util.ts diff --git a/frontend/src/components/Bids.vue b/frontend/src/components/Bids.vue index 54e8475..319f82a 100644 --- a/frontend/src/components/Bids.vue +++ b/frontend/src/components/Bids.vue @@ -54,6 +54,7 @@ import { ref } from 'vue' import api from '../utils/api' import { ElMessage } from 'element-plus' import { Paperclip } from '@element-plus/icons-vue' +import { formatDate } from '../utils/date.util' interface Props { bids: any[] @@ -72,11 +73,6 @@ const selectedSource = ref('') const currentPage = ref(1) const pageSize = ref(10) -const formatDate = (dateString: string) => { - if (!dateString) return '-' - return new Date(dateString).toLocaleDateString() -} - const handleSourceChange = () => { currentPage.value = 1 emit('fetch', currentPage.value, pageSize.value, selectedSource.value || undefined) diff --git a/frontend/src/components/CrawlInfo.vue b/frontend/src/components/CrawlInfo.vue index 58172e7..90bd473 100644 --- a/frontend/src/components/CrawlInfo.vue +++ b/frontend/src/components/CrawlInfo.vue @@ -16,12 +16,12 @@ @@ -78,6 +78,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue' import api from '../utils/api' import { ElMessage } from 'element-plus' import { Refresh } from '@element-plus/icons-vue' +import { formatDateTime } from '../utils/date.util' interface CrawlStat { source: string @@ -90,7 +91,6 @@ interface CrawlStat { const crawlStats = ref([]) const loading = ref(false) const crawlingSources = ref>(new Set()) -const REFRESH_INTERVAL = 10000 let refreshTimer: number | null = null const totalCount = computed(() => { @@ -105,18 +105,6 @@ const errorSources = computed(() => { return crawlStats.value.filter(item => item.error && item.error.trim()).length }) -const formatDate = (dateStr: string | null) => { - if (!dateStr) return '-' - const date = new Date(dateStr) - return date.toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) -} - const fetchCrawlStats = async () => { loading.value = true try { diff --git a/frontend/src/components/Dashboard.vue b/frontend/src/components/Dashboard.vue index c3fa627..f6602b5 100644 --- a/frontend/src/components/Dashboard.vue +++ b/frontend/src/components/Dashboard.vue @@ -81,6 +81,7 @@ import api from '../utils/api' import { ElMessage } from 'element-plus' import { Refresh, Paperclip } from '@element-plus/icons-vue' import PinnedProject from './PinnedProject.vue' +import { formatDate } from '../utils/date.util' interface Props { todayBids: any[] @@ -160,11 +161,6 @@ watch(dateRange, () => { } }) -const formatDate = (dateString: string) => { - if (!dateString) return '-' - return new Date(dateString).toLocaleDateString() -} - // 过滤 Today's Bids,只显示包含所选关键字的项目,并且在日期范围内 const filteredTodayBids = computed(() => { let result = props.todayBids diff --git a/frontend/src/components/PinnedProject.vue b/frontend/src/components/PinnedProject.vue index 2761d63..cb30f58 100644 --- a/frontend/src/components/PinnedProject.vue +++ b/frontend/src/components/PinnedProject.vue @@ -38,7 +38,7 @@ @@ -51,6 +51,7 @@ import { ref, onMounted } from 'vue' import api from '../utils/api' import { ElMessage } from 'element-plus' import { Loading, InfoFilled, Paperclip } from '@element-plus/icons-vue' +import { formatSimpleDate } from '../utils/date.util' const emit = defineEmits<{ pinChanged: [title: string] @@ -88,16 +89,6 @@ const togglePin = async (item: any) => { } } -// 格式化日期,只显示年月日 -const formatDate = (dateStr: string) => { - if (!dateStr) return '' - const date = new Date(dateStr) - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` -} - // 初始化时加载置顶项目 onMounted(() => { loadPinnedBids() diff --git a/frontend/src/utils/date.util.ts b/frontend/src/utils/date.util.ts new file mode 100644 index 0000000..bf509c5 --- /dev/null +++ b/frontend/src/utils/date.util.ts @@ -0,0 +1,58 @@ +/** + * 日期格式化工具函数 + * 统一处理东八区(Asia/Shanghai)时间显示 + */ + +/** + * 格式化日期为 YYYY-MM-DD 格式 + * @param dateStr 日期字符串或Date对象 + * @returns 格式化后的日期字符串 + */ +export function formatDate(dateStr: string | null | undefined): string { + if (!dateStr) return '-' + const date = new Date(dateStr) + if (isNaN(date.getTime())) return '-' + + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + timeZone: 'Asia/Shanghai' + }).replace(/\//g, '-') +} + +/** + * 格式化日期时间为 YYYY-MM-DD HH:mm 格式 + * @param dateStr 日期字符串或Date对象 + * @returns 格式化后的日期时间字符串 + */ +export function formatDateTime(dateStr: string | null | undefined): string { + if (!dateStr) return '-' + const date = new Date(dateStr) + if (isNaN(date.getTime())) return '-' + + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + timeZone: 'Asia/Shanghai' + }).replace(/\//g, '-') +} + +/** + * 格式化日期为简洁的 YYYY-MM-DD 格式(用于置顶项目等) + * @param dateStr 日期字符串或Date对象 + * @returns 格式化后的日期字符串 + */ +export function formatSimpleDate(dateStr: string | null | undefined): string { + if (!dateStr) return '' + const date = new Date(dateStr) + if (isNaN(date.getTime())) return '' + + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} diff --git a/package.json b/package.json index 7e90a94..2641976 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/main.js", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", diff --git a/src/bids/services/bid.service.ts b/src/bids/services/bid.service.ts index b14e427..98fc096 100644 --- a/src/bids/services/bid.service.ts +++ b/src/bids/services/bid.service.ts @@ -3,6 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, LessThan } from 'typeorm'; import { BidItem } from '../entities/bid-item.entity'; import { CrawlInfoAdd } from '../../crawler/entities/crawl-info-add.entity'; +import { + getDaysAgo, + setStartOfDay, + setEndOfDay, +} from '../../common/utils/timezone.util'; interface FindAllQuery { page?: number; @@ -73,8 +78,7 @@ export class BidsService { } async cleanOldData() { - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const thirtyDaysAgo = getDaysAgo(30); return this.bidRepository.delete({ createdAt: LessThan(thirtyDaysAgo), }); @@ -91,9 +95,7 @@ export class BidsService { } async getRecentBids() { - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - thirtyDaysAgo.setHours(0, 0, 0, 0); + const thirtyDaysAgo = setStartOfDay(getDaysAgo(30)); return this.bidRepository .createQueryBuilder('bid') @@ -118,14 +120,12 @@ export class BidsService { const qb = this.bidRepository.createQueryBuilder('bid'); if (startDate) { - const start = new Date(startDate); - start.setHours(0, 0, 0, 0); + const start = setStartOfDay(new Date(startDate)); qb.andWhere('bid.publishDate >= :startDate', { startDate: start }); } if (endDate) { - const end = new Date(endDate); - end.setHours(23, 59, 59, 999); + const end = setEndOfDay(new Date(endDate)); qb.andWhere('bid.publishDate <= :endDate', { endDate: end }); } diff --git a/src/common/logger/winston.config.ts b/src/common/logger/winston.config.ts index c693479..85c1d43 100644 --- a/src/common/logger/winston.config.ts +++ b/src/common/logger/winston.config.ts @@ -76,6 +76,10 @@ const errorLogTransport = new DailyRotateFile({ export const winstonLogger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: logFormat, - transports: [consoleTransport, appLogTransport as any, errorLogTransport as any], + transports: [ + consoleTransport, + appLogTransport as any, + errorLogTransport as any, + ], exitOnError: false, }); diff --git a/src/common/utils/timezone.util.ts b/src/common/utils/timezone.util.ts new file mode 100644 index 0000000..408945b --- /dev/null +++ b/src/common/utils/timezone.util.ts @@ -0,0 +1,98 @@ +/** + * 时区工具函数 + * 统一处理东八区(Asia/Shanghai)时间相关的操作 + */ + +const TIMEZONE_OFFSET = 8 * 60 * 60 * 1000; + +/** + * 获取当前时间的东八区Date对象 + * @returns Date 当前时间的东八区表示 + */ +export function getCurrentDateInTimezone(): Date { + const now = new Date(); + const utc = now.getTime() + now.getTimezoneOffset() * 60 * 1000; + return new Date(utc + TIMEZONE_OFFSET); +} + +/** + * 将任意Date对象转换为东八区时间 + * @param date 原始Date对象 + * @returns Date 转换后的东八区时间 + */ +export function convertToTimezone(date: Date): Date { + const utc = date.getTime() + date.getTimezoneOffset() * 60 * 1000; + return new Date(utc + TIMEZONE_OFFSET); +} + +/** + * 格式化日期为 YYYY-MM-DD 格式 + * @param date Date对象 + * @returns 格式化后的日期字符串 + */ +export function formatDate(date: Date): string { + const timezoneDate = convertToTimezone(date); + const year = timezoneDate.getFullYear(); + const month = String(timezoneDate.getMonth() + 1).padStart(2, '0'); + const day = String(timezoneDate.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * 格式化日期时间为 YYYY-MM-DD HH:mm:ss 格式 + * @param date Date对象 + * @returns 格式化后的日期时间字符串 + */ +export function formatDateTime(date: Date): string { + const timezoneDate = convertToTimezone(date); + const year = timezoneDate.getFullYear(); + const month = String(timezoneDate.getMonth() + 1).padStart(2, '0'); + const day = String(timezoneDate.getDate()).padStart(2, '0'); + const hours = String(timezoneDate.getHours()).padStart(2, '0'); + const minutes = String(timezoneDate.getMinutes()).padStart(2, '0'); + const seconds = String(timezoneDate.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} + +/** + * 设置时间为当天的开始时间 (00:00:00.000) + * @param date Date对象 + * @returns 设置后的Date对象 + */ +export function setStartOfDay(date: Date): Date { + const timezoneDate = convertToTimezone(date); + timezoneDate.setHours(0, 0, 0, 0); + return timezoneDate; +} + +/** + * 设置时间为当天的结束时间 (23:59:59.999) + * @param date Date对象 + * @returns 设置后的Date对象 + */ +export function setEndOfDay(date: Date): Date { + const timezoneDate = convertToTimezone(date); + timezoneDate.setHours(23, 59, 59, 999); + return timezoneDate; +} + +/** + * 获取指定天数前的日期 + * @param days 天数 + * @returns 指定天数前的Date对象 + */ +export function getDaysAgo(days: number): Date { + const date = getCurrentDateInTimezone(); + date.setDate(date.getDate() - days); + return date; +} + +/** + * 解析日期字符串为东八区Date对象 + * @param dateStr 日期字符串 (支持 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss 格式) + * @returns 解析后的Date对象 + */ +export function parseDateString(dateStr: string): Date { + const date = new Date(dateStr); + return convertToTimezone(date); +} diff --git a/src/crawler/services/cdt_target.ts b/src/crawler/services/cdt_target.ts index 88960f3..a2e190d 100644 --- a/src/crawler/services/cdt_target.ts +++ b/src/crawler/services/cdt_target.ts @@ -139,7 +139,10 @@ export const CdtCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/ceic_target.ts b/src/crawler/services/ceic_target.ts index b31bfd6..faef4cc 100644 --- a/src/crawler/services/ceic_target.ts +++ b/src/crawler/services/ceic_target.ts @@ -142,7 +142,10 @@ export const CeicCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/cgnpc_target.ts b/src/crawler/services/cgnpc_target.ts index b3b7e86..a8f9bd5 100644 --- a/src/crawler/services/cgnpc_target.ts +++ b/src/crawler/services/cgnpc_target.ts @@ -148,7 +148,10 @@ export const CgnpcCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/chdtp_target.ts b/src/crawler/services/chdtp_target.ts index fc44b03..61327e4 100644 --- a/src/crawler/services/chdtp_target.ts +++ b/src/crawler/services/chdtp_target.ts @@ -134,7 +134,10 @@ export const ChdtpCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/cnncecp_target.ts b/src/crawler/services/cnncecp_target.ts index 3b2cc82..142f132 100644 --- a/src/crawler/services/cnncecp_target.ts +++ b/src/crawler/services/cnncecp_target.ts @@ -148,7 +148,10 @@ export const CnncecpCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/cnooc_target.ts b/src/crawler/services/cnooc_target.ts index f55c8ba..d711db4 100644 --- a/src/crawler/services/cnooc_target.ts +++ b/src/crawler/services/cnooc_target.ts @@ -148,7 +148,10 @@ export const CnoocCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/eps_target.ts b/src/crawler/services/eps_target.ts index d1fd88b..05b82dd 100644 --- a/src/crawler/services/eps_target.ts +++ b/src/crawler/services/eps_target.ts @@ -148,7 +148,10 @@ export const EpsCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/powerbeijing_target.ts b/src/crawler/services/powerbeijing_target.ts index eb65ea0..f9cec32 100644 --- a/src/crawler/services/powerbeijing_target.ts +++ b/src/crawler/services/powerbeijing_target.ts @@ -148,7 +148,10 @@ export const PowerbeijingCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/sdicc_target.ts b/src/crawler/services/sdicc_target.ts index 7eb0ae2..5188823 100644 --- a/src/crawler/services/sdicc_target.ts +++ b/src/crawler/services/sdicc_target.ts @@ -148,7 +148,10 @@ export const SdiccCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/crawler/services/szecp_target.ts b/src/crawler/services/szecp_target.ts index dc241e4..3afae2f 100644 --- a/src/crawler/services/szecp_target.ts +++ b/src/crawler/services/szecp_target.ts @@ -142,7 +142,10 @@ export const SzecpCrawler = { logger.log(`Navigating to ${this.url}...`); await delayRetry( async () => { - await page.goto(this.url, { waitUntil: 'networkidle2', timeout: 60000 }); + await page.goto(this.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); }, 3, 5000, diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 8ab107e..5b8ba65 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -20,6 +20,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; database: configService.get('DATABASE_NAME', 'bidding'), entities: [__dirname + '/../**/*.entity{.ts,.js}'], synchronize: false, + timezone: '+08:00', }), }), ],