fix(db): 统一字符集排序规则并修复时间戳类型
- 新增 SQL 脚本统一表与列字符集为 utf8mb4_unicode_ci - 移除建表语句冗余 COLLATE 子句,由全局配置控制 - 将挑战起止时间字段由 Date 改为 BIGINT 时间戳,避免时区与精度问题 - 补充 Winston 日志追踪挑战详情查询性能 - 数据库模块新增 charset 与 collate 全局配置,确保后续表一致性 BREAKING CHANGE: challenge.startAt/endAt 由 Date 变更为 number(毫秒时间戳),调用方需同步调整类型
This commit is contained in:
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS t_challenges (
|
|||||||
cta_label VARCHAR(128) NOT NULL COMMENT 'CTA 按钮文字',
|
cta_label VARCHAR(128) NOT NULL COMMENT 'CTA 按钮文字',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 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 (
|
CREATE TABLE IF NOT EXISTS t_challenge_participants (
|
||||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
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_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 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)
|
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
|
CREATE INDEX idx_challenge_participants_status_progress
|
||||||
ON t_challenge_participants (challenge_id, status, progress_value DESC, updated_at ASC);
|
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_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 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)
|
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
|
||||||
41
sql-scripts/fix-collation.sql
Normal file
41
sql-scripts/fix-collation.sql
Normal file
@@ -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');
|
||||||
@@ -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 { InjectModel } from '@nestjs/sequelize';
|
||||||
import { Challenge, ChallengeStatus } from './models/challenge.model';
|
import { Challenge, ChallengeStatus } from './models/challenge.model';
|
||||||
import { ChallengeParticipant, ChallengeParticipantStatus } from './models/challenge-participant.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 { fn, col, Op, UniqueConstraintError } from 'sequelize';
|
||||||
import * as dayjs from 'dayjs';
|
import * as dayjs from 'dayjs';
|
||||||
import { User } from '../users/models/user.model';
|
import { User } from '../users/models/user.model';
|
||||||
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { Logger as WinstonLogger } from 'winston';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChallengesService {
|
export class ChallengesService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger,
|
||||||
|
|
||||||
@InjectModel(Challenge)
|
@InjectModel(Challenge)
|
||||||
private readonly challengeModel: typeof Challenge,
|
private readonly challengeModel: typeof Challenge,
|
||||||
@InjectModel(ChallengeParticipant)
|
@InjectModel(ChallengeParticipant)
|
||||||
@@ -100,6 +105,13 @@ export class ChallengesService {
|
|||||||
throw new NotFoundException('挑战不存在');
|
throw new NotFoundException('挑战不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.winstonLogger.info('start get detail', {
|
||||||
|
context: 'getChallengeDetail',
|
||||||
|
userId,
|
||||||
|
challengeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const [participantsCount, participation] = await Promise.all([
|
const [participantsCount, participation] = await Promise.all([
|
||||||
this.participantModel.count({
|
this.participantModel.count({
|
||||||
where: {
|
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({
|
const rankingsRaw = await this.participantModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
challengeId,
|
challengeId,
|
||||||
@@ -131,6 +152,15 @@ export class ChallengesService {
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.winstonLogger.info('get rankingsRaw end', {
|
||||||
|
context: 'getChallengeDetail',
|
||||||
|
userId,
|
||||||
|
challengeId,
|
||||||
|
participantsCount,
|
||||||
|
participation,
|
||||||
|
rankingsRawCount: rankingsRaw.length,
|
||||||
|
});
|
||||||
|
|
||||||
const progress = participation
|
const progress = participation
|
||||||
? this.buildChallengeProgress(participation.progressValue, participation.targetValue, challenge.progressUnit)
|
? this.buildChallengeProgress(participation.progressValue, participation.targetValue, challenge.progressUnit)
|
||||||
: undefined;
|
: 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 now = dayjs();
|
||||||
const start = dayjs(startAt);
|
const start = dayjs(startAt);
|
||||||
const end = dayjs(endAt);
|
const end = dayjs(endAt);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export interface ChallengeListItemDto {
|
|||||||
durationLabel: string;
|
durationLabel: string;
|
||||||
requirementLabel: string;
|
requirementLabel: string;
|
||||||
status: ChallengeStatus;
|
status: ChallengeStatus;
|
||||||
startAt: Date;
|
startAt: number;
|
||||||
endAt: Date;
|
endAt: number;
|
||||||
participantsCount: number;
|
participantsCount: number;
|
||||||
rankingDescription: string | null;
|
rankingDescription: string | null;
|
||||||
highlightTitle: string;
|
highlightTitle: string;
|
||||||
|
|||||||
@@ -34,18 +34,18 @@ export class Challenge extends Model {
|
|||||||
declare image: string;
|
declare image: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.DATE,
|
type: DataType.BIGINT,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '挑战开始时间',
|
comment: '挑战开始时间(时间戳)',
|
||||||
})
|
})
|
||||||
declare startAt: Date;
|
declare startAt: number;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.DATE,
|
type: DataType.BIGINT,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '挑战结束时间',
|
comment: '挑战结束时间(时间戳)',
|
||||||
})
|
})
|
||||||
declare endAt: Date;
|
declare endAt: number;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.STRING(128),
|
type: DataType.STRING(128),
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
username: configService.get('DB_USERNAME'),
|
username: configService.get('DB_USERNAME'),
|
||||||
password: configService.get('DB_PASSWORD'),
|
password: configService.get('DB_PASSWORD'),
|
||||||
database: configService.get('DB_DATABASE'),
|
database: configService.get('DB_DATABASE'),
|
||||||
|
dialectOptions: {
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
collate: 'utf8mb4_0900_ai_ci',
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
collate: 'utf8mb4_0900_ai_ci',
|
||||||
|
},
|
||||||
autoLoadModels: true,
|
autoLoadModels: true,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user