From 22fcf694a67e024c90592e8f6ce0ed3050ceffde Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 29 Sep 2025 09:59:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(db):=20=E7=BB=9F=E4=B8=80=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E9=9B=86=E6=8E=92=E5=BA=8F=E8=A7=84=E5=88=99=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=97=B6=E9=97=B4=E6=88=B3=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SQL 脚本统一表与列字符集为 utf8mb4_unicode_ci - 移除建表语句冗余 COLLATE 子句,由全局配置控制 - 将挑战起止时间字段由 Date 改为 BIGINT 时间戳,避免时区与精度问题 - 补充 Winston 日志追踪挑战详情查询性能 - 数据库模块新增 charset 与 collate 全局配置,确保后续表一致性 BREAKING CHANGE: challenge.startAt/endAt 由 Date 变更为 number(毫秒时间戳),调用方需同步调整类型 --- sql-scripts/challenges.sql | 6 ++-- sql-scripts/fix-collation.sql | 41 ++++++++++++++++++++++++ src/challenges/challenges.service.ts | 34 ++++++++++++++++++-- src/challenges/dto/challenge-list.dto.ts | 4 +-- src/challenges/models/challenge.model.ts | 12 +++---- src/database/database.module.ts | 8 +++++ 6 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 sql-scripts/fix-collation.sql diff --git a/sql-scripts/challenges.sql b/sql-scripts/challenges.sql index ee0cadf..07df2c4 100644 --- a/sql-scripts/challenges.sql +++ b/sql-scripts/challenges.sql @@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS t_challenges ( cta_label VARCHAR(128) NOT NULL COMMENT 'CTA 按钮文字', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB CREATE TABLE IF NOT EXISTS t_challenge_participants ( id CHAR(36) NOT NULL PRIMARY KEY, @@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS t_challenge_participants ( CONSTRAINT fk_challenge_participant_challenge FOREIGN KEY (challenge_id) REFERENCES t_challenges (id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_challenge_participant_user FOREIGN KEY (user_id) REFERENCES t_users (id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT uq_challenge_participant UNIQUE KEY (challenge_id, user_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB CREATE INDEX idx_challenge_participants_status_progress ON t_challenge_participants (challenge_id, status, progress_value DESC, updated_at ASC); @@ -53,4 +53,4 @@ CREATE TABLE IF NOT EXISTS t_challenge_progress_reports ( CONSTRAINT fk_challenge_progress_reports_challenge FOREIGN KEY (challenge_id) REFERENCES t_challenges (id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_challenge_progress_reports_user FOREIGN KEY (user_id) REFERENCES t_users (id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT uq_challenge_progress_reports_day UNIQUE KEY (challenge_id, user_id, report_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB \ No newline at end of file diff --git a/sql-scripts/fix-collation.sql b/sql-scripts/fix-collation.sql new file mode 100644 index 0000000..1f1ebb0 --- /dev/null +++ b/sql-scripts/fix-collation.sql @@ -0,0 +1,41 @@ +-- 修复字符集排序规则不一致的问题 +-- 将所有相关表的字符集统一为 utf8mb4_unicode_ci + +-- 检查当前表的字符集和排序规则 +SELECT + TABLE_NAME, + TABLE_COLLATION, + CHARACTER_SET_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = DATABASE() +AND TABLE_NAME IN ('t_users', 't_challenge_participants', 't_challenges'); + +-- 检查列的字符集和排序规则 +SELECT + TABLE_NAME, + COLUMN_NAME, + COLLATION_NAME, + CHARACTER_SET_NAME +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() +AND TABLE_NAME IN ('t_users', 't_challenge_participants', 't_challenges') +AND COLUMN_NAME IN ('id', 'user_id', 'challenge_id'); + +-- 修改表字符集和排序规则 +ALTER TABLE t_users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE t_challenge_participants CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE t_challenges CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 修改特定列的字符集和排序规则(如果需要) +ALTER TABLE t_users MODIFY id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE t_challenge_participants MODIFY user_id VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE t_challenge_participants MODIFY challenge_id CHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 验证修复结果 +SELECT + TABLE_NAME, + TABLE_COLLATION, + CHARACTER_SET_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = DATABASE() +AND TABLE_NAME IN ('t_users', 't_challenge_participants', 't_challenges'); \ No newline at end of file diff --git a/src/challenges/challenges.service.ts b/src/challenges/challenges.service.ts index 0ed55b3..5b7ef3f 100644 --- a/src/challenges/challenges.service.ts +++ b/src/challenges/challenges.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, ConflictException, Inject } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { Challenge, ChallengeStatus } from './models/challenge.model'; import { ChallengeParticipant, ChallengeParticipantStatus } from './models/challenge-participant.model'; @@ -10,10 +10,15 @@ import { ChallengeProgressDto, RankingItemDto } from './dto/challenge-progress.d import { fn, col, Op, UniqueConstraintError } from 'sequelize'; import * as dayjs from 'dayjs'; import { User } from '../users/models/user.model'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { Logger as WinstonLogger } from 'winston'; @Injectable() export class ChallengesService { + constructor( + @Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger, + @InjectModel(Challenge) private readonly challengeModel: typeof Challenge, @InjectModel(ChallengeParticipant) @@ -100,6 +105,13 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + this.winstonLogger.info('start get detail', { + context: 'getChallengeDetail', + userId, + challengeId, + }); + + const [participantsCount, participation] = await Promise.all([ this.participantModel.count({ where: { @@ -118,6 +130,15 @@ export class ChallengesService { }), ]); + this.winstonLogger.info('end get detail', { + context: 'getChallengeDetail', + userId, + challengeId, + participantsCount, + participation, + }); + + const rankingsRaw = await this.participantModel.findAll({ where: { challengeId, @@ -131,6 +152,15 @@ export class ChallengesService { limit: 10, }); + this.winstonLogger.info('get rankingsRaw end', { + context: 'getChallengeDetail', + userId, + challengeId, + participantsCount, + participation, + rankingsRawCount: rankingsRaw.length, + }); + const progress = participation ? this.buildChallengeProgress(participation.progressValue, participation.targetValue, challenge.progressUnit) : undefined; @@ -333,7 +363,7 @@ export class ChallengesService { }; } - private computeStatus(startAt: Date, endAt: Date): ChallengeStatus { + private computeStatus(startAt: number, endAt: number): ChallengeStatus { const now = dayjs(); const start = dayjs(startAt); const end = dayjs(endAt); diff --git a/src/challenges/dto/challenge-list.dto.ts b/src/challenges/dto/challenge-list.dto.ts index 4cc8aa4..54ebdc1 100644 --- a/src/challenges/dto/challenge-list.dto.ts +++ b/src/challenges/dto/challenge-list.dto.ts @@ -9,8 +9,8 @@ export interface ChallengeListItemDto { durationLabel: string; requirementLabel: string; status: ChallengeStatus; - startAt: Date; - endAt: Date; + startAt: number; + endAt: number; participantsCount: number; rankingDescription: string | null; highlightTitle: string; diff --git a/src/challenges/models/challenge.model.ts b/src/challenges/models/challenge.model.ts index eb376fd..304aec9 100644 --- a/src/challenges/models/challenge.model.ts +++ b/src/challenges/models/challenge.model.ts @@ -34,18 +34,18 @@ export class Challenge extends Model { declare image: string; @Column({ - type: DataType.DATE, + type: DataType.BIGINT, allowNull: false, - comment: '挑战开始时间', + comment: '挑战开始时间(时间戳)', }) - declare startAt: Date; + declare startAt: number; @Column({ - type: DataType.DATE, + type: DataType.BIGINT, allowNull: false, - comment: '挑战结束时间', + comment: '挑战结束时间(时间戳)', }) - declare endAt: Date; + declare endAt: number; @Column({ type: DataType.STRING(128), diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 8845a99..d732fe2 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -13,6 +13,14 @@ import { ConfigService } from '@nestjs/config'; username: configService.get('DB_USERNAME'), password: configService.get('DB_PASSWORD'), database: configService.get('DB_DATABASE'), + dialectOptions: { + charset: 'utf8mb4', + collate: 'utf8mb4_0900_ai_ci', + }, + define: { + charset: 'utf8mb4', + collate: 'utf8mb4_0900_ai_ci', + }, autoLoadModels: true, synchronize: true, }),