Skip to content

Commit 4dfb4dc

Browse files
authored
Merge pull request #431 from SageSeekerSociety/dev
chore(release): 0.12.1
2 parents a7b86ba + f204c30 commit 4dfb4dc

File tree

7 files changed

+933
-483
lines changed

7 files changed

+933
-483
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"eslint-plugin-prettier": "^5.2.3",
9595
"husky": "^9.1.7",
9696
"jest": "^29.7.0",
97+
"jest-mock-extended": "4.0.0-beta1",
9798
"lint-staged": "^15.2.11",
9899
"prettier": "^3.5.1",
99100
"prisma": "^5.22.0",

pnpm-lock.yaml

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common/config/configuration.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import { ConfigService } from '@nestjs/config';
22
import { ElasticsearchModuleOptions } from '@nestjs/elasticsearch';
33

4+
export interface RedisConfig {
5+
host: string;
6+
port: number;
7+
username?: string;
8+
password?: string;
9+
db?: number;
10+
}
11+
412
export default () => {
513
return {
614
port: parseInt(process.env.PORT || '3000', 10),
15+
redis: {
16+
host: process.env.REDIS_HOST || 'localhost',
17+
port: parseInt(process.env.REDIS_PORT || '6379', 10),
18+
username: process.env.REDIS_USERNAME || undefined,
19+
password: process.env.REDIS_PASSWORD || undefined,
20+
db: process.env.REDIS_DB ? parseInt(process.env.REDIS_DB, 10) : undefined,
21+
},
722
elasticsearch: {
823
node: process.env.ELASTICSEARCH_NODE,
924
maxRetries: parseInt(process.env.ELASTICSEARCH_MAX_RETRIES || '3', 10),
@@ -42,7 +57,7 @@ export default () => {
4257
process.env.TOTP_BACKUP_CODES_COUNT || '10',
4358
10,
4459
),
45-
window: parseInt(process.env.TOTP_WINDOW || '1', 10), // 验证窗口,默认前后1个时间窗口
60+
window: parseInt(process.env.TOTP_WINDOW || '1', 10),
4661
},
4762
disableEmailVerification: process.env.DISABLE_EMAIL_VERIFICATION === 'true',
4863
};
@@ -56,3 +71,9 @@ export function elasticsearchConfigFactory(
5671
throw new Error('Elasticsearch configuration not found');
5772
return config;
5873
}
74+
75+
export function redisConfigFactory(configService: ConfigService): RedisConfig {
76+
const config = configService.get<RedisConfig>('redis');
77+
if (config == undefined) throw new Error('Redis configuration not found');
78+
return config;
79+
}

src/common/helper/file.helper.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import crypto from 'crypto';
22
import * as fs from 'fs';
33
import mime from 'mime-types';
44

5-
export function getFileHash(filePath: string): Promise<string> {
5+
export function getFileHash(
6+
filePath: string,
7+
algorithm: 'sha256' | 'md5' = 'sha256',
8+
): Promise<string> {
69
return new Promise((resolve, reject) => {
7-
const hash = crypto.createHash('sha256');
10+
const hash = crypto.createHash(algorithm);
811
const stream = fs.createReadStream(filePath);
912
stream
1013
.on('data', (data) => {

src/materials/materials.service.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
1111
import { MaterialType } from '@prisma/client';
1212
import ffmpeg from 'fluent-ffmpeg';
1313
import path, { join } from 'node:path';
14-
import { readFileSync } from 'node:fs';
1514
import { promisify } from 'node:util';
15+
import { getFileHash } from '../common/helper/file.helper';
1616
import { PrismaService } from '../common/prisma/prisma.service';
17-
import { MaterialNotFoundError, MetaDataParseError } from './materials.error';
18-
import { materialDto } from './DTO/material.dto';
1917
import { UsersService } from '../users/users.service';
20-
import md5 from 'md5';
18+
import { materialDto } from './DTO/material.dto';
19+
import { MaterialNotFoundError, MetaDataParseError } from './materials.error';
2120
@Injectable()
2221
export class MaterialsService implements OnModuleInit {
2322
private ffprobeAsync: (file: string) => Promise<ffmpeg.FfprobeData>;
@@ -101,8 +100,7 @@ export class MaterialsService implements OnModuleInit {
101100
): Promise<PrismaJson.metaType> {
102101
let meta: PrismaJson.metaType;
103102

104-
const buf = readFileSync(file.path);
105-
const hash = md5(buf);
103+
const hash = await getFileHash(file.path, 'md5');
106104

107105
if (type === MaterialType.image) {
108106
const metadata = await this.getImageMetadata(file.path);

src/users/users.service.ts

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*
88
*/
99

10+
import { RedisService } from '@liaoliaots/nestjs-redis';
1011
import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
1112
import { ConfigService } from '@nestjs/config';
1213
import {
@@ -30,6 +31,7 @@ import {
3031
import bcrypt from 'bcryptjs';
3132
import { isEmail } from 'class-validator';
3233
import { Request } from 'express';
34+
import Redis from 'ioredis';
3335
import assert from 'node:assert';
3436
import { AnswerService } from '../answer/answer.service';
3537
import {
@@ -81,9 +83,15 @@ import {
8183
UsernameNotFoundError,
8284
} from './users.error';
8385

86+
const USER_PROFILE_UPDATE_CHANNEL = 'cache:user:updated';
87+
8488
@Injectable()
8589
export class UsersService {
90+
private readonly logger = new Logger(UsersService.name);
91+
private readonly redis: Redis;
92+
8693
constructor(
94+
private readonly redisService: RedisService,
8795
private readonly emailService: EmailService,
8896
private readonly emailRuleService: EmailRuleService,
8997
private readonly authService: AuthService,
@@ -100,7 +108,9 @@ export class UsersService {
100108
private readonly prismaService: PrismaService,
101109
private readonly totpService: TOTPService,
102110
private readonly srpService: SrpService,
103-
) {}
111+
) {
112+
this.redis = this.redisService.getOrThrow();
113+
}
104114

105115
private readonly passwordResetEmailValidSeconds = 10 * 60; // 10 minutes
106116

@@ -1039,23 +1049,70 @@ export class UsersService {
10391049
intro: string,
10401050
avatarId: number,
10411051
): Promise<void> {
1052+
this.logger.log(`Attempting to update profile for user ID: ${userId}`);
10421053
const [, profile] =
10431054
await this.findUserRecordAndProfileRecordOrThrow(userId);
1044-
if ((await this.avatarsService.isAvatarExists(avatarId)) == false) {
1055+
1056+
if ((await this.avatarsService.isAvatarExists(avatarId)) === false) {
1057+
this.logger.warn(`Avatar not found: ${avatarId} for user: ${userId}`);
10451058
throw new AvatarNotFoundError(avatarId);
10461059
}
1047-
await this.avatarsService.plusUsageCount(avatarId);
1048-
await this.avatarsService.minusUsageCount(profile.avatarId);
1049-
await this.prismaService.userProfile.update({
1050-
where: {
1051-
userId,
1052-
},
1053-
data: {
1054-
nickname,
1055-
intro,
1056-
avatarId,
1057-
},
1058-
});
1060+
1061+
const oldAvatarId = profile.avatarId;
1062+
1063+
if (
1064+
profile.nickname !== nickname ||
1065+
profile.intro !== intro ||
1066+
profile.avatarId !== avatarId
1067+
) {
1068+
await this.prismaService.userProfile.update({
1069+
where: {
1070+
userId,
1071+
},
1072+
data: {
1073+
nickname,
1074+
intro,
1075+
avatarId,
1076+
},
1077+
});
1078+
this.logger.log(`Profile successfully updated for user ID: ${userId}`);
1079+
1080+
try {
1081+
const publishedCount = await this.redis.publish(
1082+
USER_PROFILE_UPDATE_CHANNEL,
1083+
userId.toString(),
1084+
);
1085+
this.logger.log(
1086+
`Published cache invalidation message for user ID: ${userId} to channel '${USER_PROFILE_UPDATE_CHANNEL}'. Received by ${publishedCount} clients.`,
1087+
);
1088+
} catch (error) {
1089+
this.logger.error(
1090+
`Failed to publish cache invalidation message for user ID: ${userId} to Redis channel '${USER_PROFILE_UPDATE_CHANNEL}'`,
1091+
error instanceof Error ? error.stack : error,
1092+
);
1093+
}
1094+
1095+
if (oldAvatarId !== avatarId) {
1096+
try {
1097+
await Promise.all([
1098+
this.avatarsService.plusUsageCount(avatarId),
1099+
this.avatarsService.minusUsageCount(oldAvatarId),
1100+
]);
1101+
this.logger.log(
1102+
`Updated avatar usage counts for user ID: ${userId}. New: ${avatarId}, Old: ${oldAvatarId}`,
1103+
);
1104+
} catch (avatarError) {
1105+
this.logger.error(
1106+
`Error updating avatar usage counts for user ID ${userId}`,
1107+
avatarError instanceof Error ? avatarError.stack : avatarError,
1108+
);
1109+
}
1110+
}
1111+
} else {
1112+
this.logger.log(
1113+
`Profile data unchanged for user ID: ${userId}. Skipping update and cache invalidation.`,
1114+
);
1115+
}
10591116
}
10601117

10611118
async getUniqueFollowRelationship(

0 commit comments

Comments
 (0)