Skip to content

Commit

Permalink
feat: add scheduled task module and weekly reset ranking function (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhuzhux authored Feb 29, 2024
1 parent 24ac0d6 commit 8164f64
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 105 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
test:
runs-on: ubuntu-latest
env:
DATABASE_URL: mysql://root:password@localhost:3307/earthworm_test
SECRET: sjldk92#sd903mnc./xklsjdf9sdfj
DATABASE_URL: mysql://root:password@localhost:3307/earthworm_test
SECRET: sjldk92#sd903mnc./xklsjdf9sdfj

services:
# 定义测试专用的 MySQL 服务
Expand All @@ -28,14 +28,13 @@ jobs:
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "20.11.0"
node-version: "20.11.0"

- name: Install pnpm
run: npm install -g pnpm
Expand All @@ -46,7 +45,7 @@ jobs:
- name: Run database initialization command
run: pnpm db:init:test:ci

- name: Run build schema
- name: Run build schema
run: pnpm schema:build

- name: Run tests
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.0.1",
"@nestjs/swagger": "^7.1.17",
"argon2": "^0.31.2",
"class-transformer": "^0.5.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ToolModule } from '../tool/tool.module';
import { RedisModule } from '@nestjs-modules/ioredis';
import { RankModule } from '../rank/rank.module';
import { GameModule } from '../game/game.module';
import { ScheduleModule } from '@nestjs/schedule';
import { CronJobModule } from '../cron-job/cron-job.module';

@Module({
imports: [
Expand All @@ -19,13 +21,15 @@ import { GameModule } from '../game/game.module';
ToolModule,
RankModule,
GameModule,
CronJobModule,
RedisModule.forRootAsync({
useFactory: () => ({
type: 'single',
url: process.env.REDIS_URL,
password: process.env.REDIS_PASSWORD,
}),
}),
ScheduleModule.forRoot(),
],
})
export class AppModule {}
8 changes: 8 additions & 0 deletions apps/api/src/cron-job/cron-job.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { CronJobService } from './cron-job.service';
import { RankService } from '../rank/rank.service';

@Module({
providers: [CronJobService, RankService],
})
export class CronJobModule {}
15 changes: 15 additions & 0 deletions apps/api/src/cron-job/cron-job.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { RankService } from '../rank/rank.service';

@Injectable()
export class CronJobService {
private static readonly EVERY_MONDAY_AT_2AM = '0 2 * * 1';

constructor(private readonly rankService: RankService) {}

@Cron(CronJobService.EVERY_MONDAY_AT_2AM)
async resetRankList() {
this.rankService.resetRankList();
}
}
20 changes: 15 additions & 5 deletions apps/api/src/rank/rank.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import Redis from 'ioredis';
import { UserEntity } from '../user/user.decorators';

@Injectable()
export class RankService {
private readonly FINISH_COUNT_KEY = `user:finishCount`;
private readonly logger = new Logger(RankService.name);

constructor(@InjectRedis() private readonly redis: Redis) {}

Expand All @@ -26,10 +27,10 @@ export class RankService {
}

private translateList(rankList: string[]) {
let res = [];
const res = [];
for (let i = 0; i < rankList.length; i += 2) {
let username = this.getUserName(rankList[i]);
let count = rankList[i + 1];
const username = this.getUserName(rankList[i]);
const count = rankList[i + 1];
res.push({ username, count });
}
return res;
Expand All @@ -38,7 +39,7 @@ export class RankService {
// return top 10 and self rank
async getRankList(user: UserEntity) {
// return [member, count, member, count, ...]
let rankList = await this.redis.zrevrange(
const rankList = await this.redis.zrevrange(
this.FINISH_COUNT_KEY,
0,
9,
Expand All @@ -62,4 +63,13 @@ export class RankService {
self,
};
}

async resetRankList() {
try {
await this.redis.del(this.FINISH_COUNT_KEY);
this.logger.verbose(`每周重置排行榜成功: ${new Date()}`);
} catch (error) {
this.logger.error(`重置排行榜时发生错误: ${error}`);
}
}
}
90 changes: 90 additions & 0 deletions apps/api/src/rank/tests/rank.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RankController } from '../rank.controller';
import { RankService } from '../rank.service';
import { MockRedisModule } from '../../../test/helper/mockRedis';
import { JwtModule } from '@nestjs/jwt';
import { createUser } from '../../../test/fixture/user';
import {
createEmptyRankList,
createRankListWithFirstUserFinishedCourse,
createRankListWithUserFinishedCourse2Times,
} from '../../../test/fixture/rank';

const user = createUser();
const emptyRankList = createEmptyRankList();
const firstUserFinished = createRankListWithFirstUserFinishedCourse();
const userFinishedTwice = createRankListWithUserFinishedCourse2Times();

describe('rank controller', () => {
let rankController: RankController;
let rankService: RankService;

beforeEach(async () => {
const testHelper = await setupTesting();

rankService = testHelper.rankService;
rankController = testHelper.rankController;
});

it('should return empty rank list', async () => {
const result = emptyRankList;
jest
.spyOn(rankService, 'getRankList')
.mockImplementation(async () => result);

const res = await rankController.getRankList(user);
expect(res).toBe(result);
expect(rankService.getRankList).toHaveBeenCalled();
});

it('should return rank list with first user finished course', async () => {
const result = userFinishedTwice;
jest
.spyOn(rankService, 'getRankList')
.mockImplementation(async () => result);

const res = await rankController.getRankList(user);
expect(res).toBe(result);
expect(rankService.getRankList).toHaveBeenCalled();
});

it('should return rank list with user finished course 2 times', async () => {
const result = firstUserFinished;
jest
.spyOn(rankService, 'getRankList')
.mockImplementation(async () => result);

const res = await rankController.getRankList(user);
expect(res).toBe(result);
expect(rankService.getRankList).toHaveBeenCalled();
});
});

async function setupTesting() {
const mockRankService = {
getRankList: jest.fn(() => {
return firstUserFinished;
}),
};

const moduleRef: TestingModule = await Test.createTestingModule({
imports: [
MockRedisModule,
JwtModule.register({
secret: process.env.SECRET,
signOptions: { expiresIn: '7d' },
}),
],
controllers: [RankController],
providers: [
{
provide: RankService,
useValue: mockRankService,
},
],
}).compile();
return {
rankController: moduleRef.get<RankController>(RankController),
rankService: moduleRef.get<RankService>(RankService),
};
}
82 changes: 82 additions & 0 deletions apps/api/src/rank/tests/rank.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RankService } from '../rank.service';
import { MockRedisModule } from '../../../test/helper/mockRedis';
import { JwtModule } from '@nestjs/jwt';
import { createUser } from '../../../test/fixture/user';
import {
createEmptyRankList,
createRankListWithFirstUserFinishedCourse,
createRankListWithUserFinishedCourse2Times,
} from '../../../test/fixture/rank';

const user = createUser();
const emptyRankList = createEmptyRankList();
const firstUserFinished = createRankListWithFirstUserFinishedCourse();
const userFinishedTwice = createRankListWithUserFinishedCourse2Times();

describe('rank service', () => {
let rankService: RankService;

beforeEach(async () => {
const testHelper = await setupTesting();

rankService = testHelper.rankService;
await rankService.resetRankList();
});

afterAll(async () => {
await rankService.resetRankList();
});

describe('RankList', () => {
it('should return empty rank list', async () => {
await rankService.resetRankList();
const res = await rankService.getRankList(user);

expect(res).toEqual(emptyRankList);
});

it('should return rank list with first user finished course', async () => {
await rankService.userFinishCourse(user.userId, user.username);
const res = await rankService.getRankList(user);

expect(res).toEqual(firstUserFinished);

await rankService.resetRankList();
});

it('should return rank list with user finished course 2 times', async () => {
await rankService.userFinishCourse(user.userId, user.username);
await rankService.userFinishCourse(user.userId, user.username);
const res = await rankService.getRankList(user);

expect(res).toEqual(userFinishedTwice);

await rankService.resetRankList();
});

it('should return empty rank list after reset', async () => {
await rankService.userFinishCourse(user.userId, user.username);
await rankService.resetRankList();
const res = await rankService.getRankList(user);

expect(res).toEqual(emptyRankList);
});
});
});

async function setupTesting() {
const moduleRef: TestingModule = await Test.createTestingModule({
imports: [
MockRedisModule,
JwtModule.register({
secret: process.env.SECRET,
signOptions: { expiresIn: '7d' },
}),
],
providers: [RankService],
}).compile();
return {
rankService: moduleRef.get<RankService>(RankService),
};
}
42 changes: 42 additions & 0 deletions apps/api/test/fixture/rank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export function createEmptyRankList() {
return {
list: [],
self: {
username: 'testUser',
count: 0,
rank: null,
},
};
}

export function createRankListWithFirstUserFinishedCourse() {
return {
list: [
{
username: 'testUser',
count: '1',
},
],
self: {
username: 'testUser',
count: '1',
rank: 0,
},
};
}

export function createRankListWithUserFinishedCourse2Times() {
return {
list: [
{
username: 'testUser',
count: '2',
},
],
self: {
username: 'testUser',
count: '2',
rank: 0,
},
};
}
Loading

0 comments on commit 8164f64

Please sign in to comment.