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 @@
- {{ formatDate(row.latestUpdate) }}
+ {{ formatDateTime(row.latestUpdate) }}
- {{ formatDate(row.latestPublishDate) }}
+ {{ formatDateTime(row.latestPublishDate) }}
@@ -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 @@
- {{ formatDate(scope.row.publishDate) }}
+ {{ formatSimpleDate(scope.row.publishDate) }}
@@ -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',
}),
}),
],