diff --git a/AGENTS.md b/AGENTS.md
index 6652408..1960dab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,7 +45,7 @@
# Memory Context
-# $CMEM MemeMind-Server 2026-05-10 9:40pm GMT+8
+# $CMEM MemeMind-Server 2026-05-13 8:45am GMT+8
No previous sessions found.
\ No newline at end of file
diff --git a/docs/api/share-challenge-api.md b/docs/api/share-challenge-api.md
index 57d2782..34bd534 100644
--- a/docs/api/share-challenge-api.md
+++ b/docs/api/share-challenge-api.md
@@ -4,6 +4,8 @@
## Changelog
+- 2026-05-13: 新增 `GET /api/v1/share/{code}` 分享挑战详情接口,返回挑战基本信息和所有已提交用户的排行榜;排行榜项包含用户头像、昵称、答对题数和总耗时。
+- 2026-05-12: `GET /api/v1/share/created` 响应新增 `firstPlaceUser`,返回每个挑战当前第一名用户的昵称和头像;暂无提交结果时为 `null`。
- 2026-05-10: 移除 `POST /api/v1/share/progress` 单关进度上报接口;新增 `POST /api/v1/share/{code}/submit`,客户端一次提交整场挑战的每关答案和耗时,服务端校验答案后返回排名、答对题数、参与人数和完整关卡答案,并持久化提交结果。
## 目录
@@ -27,8 +29,9 @@
1. **关卡时间限制**:`levels` 表新增 `time_limit` 字段,支持关卡通关时间限制
2. **整场挑战提交**:`POST /api/v1/share/{code}/submit` 接口,用于一次性提交分享挑战中每一关的答案和耗时
3. **挑战结果返回**:提交后返回当前用户排名、答对题数、参与人数、总耗时和每关校验结果
-4. **我创建的挑战列表**:`GET /api/v1/share/created` 接口,用于查询当前用户创建过的分享挑战、参与人数和本人排名
-5. **关卡排序**:关卡全局顺序按 `levels.sort_key` 的应用层字节序计算,接口中的 `sortOrder` 为排序后的 0-based 连续序号
+4. **分享挑战详情**:`GET /api/v1/share/{code}` 接口,用于查询单个挑战的基本信息和完整排行榜
+5. **我创建的挑战列表**:`GET /api/v1/share/created` 接口,用于查询当前用户创建过的分享挑战、参与人数、本人排名和当前第一名用户信息
+6. **关卡排序**:关卡全局顺序按 `levels.sort_key` 的应用层字节序计算,接口中的 `sortOrder` 为排序后的 0-based 连续序号
---
@@ -296,7 +299,105 @@ Content-Type: application/json
---
-### 4. 获取我创建的分享挑战
+### 4. 获取分享挑战详情
+
+获取单个分享挑战的基本信息,以及该挑战下所有已提交结果用户的排行榜。
+
+**接口地址**:`GET /api/v1/share/{code}`
+
+**是否需要认证**:是(JWT Bearer Token)
+
+**路径参数**:
+
+| 参数 | 类型 | 必填 | 说明 |
+| ---- | ------ | ---- | -------------- |
+| code | string | 是 | 分享码(8 位) |
+
+**响应数据**:
+
+```typescript
+{
+ id: string; // 分享挑战 ID
+ shareCode: string; // 分享码
+ title: string; // 分享标题
+ levelCount: number; // 挑战关卡总数
+ participantCount: number; // 已提交结果的参与用户总数
+ userRank: number | null; // 当前用户排名;尚未提交结果时为 null
+ createdAt: string; // 创建时间,ISO 8601 字符串
+ rankings: [
+ {
+ rank: number; // 名次,从 1 开始
+ participantId: string; // 参与用户 ID
+ nickname: string | null;
+ avatarUrl: string | null;
+ correctCount: number; // 答对题数
+ totalTimeSpent: number;// 总耗时(秒)
+ }
+ ];
+}
+```
+
+**成功响应示例**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "id": "share_001",
+ "shareCode": "abc12345",
+ "title": "我的挑战",
+ "levelCount": 6,
+ "participantCount": 3,
+ "userRank": 2,
+ "createdAt": "2026-04-13T10:00:00.000Z",
+ "rankings": [
+ {
+ "rank": 1,
+ "participantId": "user_002",
+ "nickname": "速度玩家",
+ "avatarUrl": "https://example.com/avatar-speed.png",
+ "correctCount": 6,
+ "totalTimeSpent": 90
+ },
+ {
+ "rank": 2,
+ "participantId": "user_001",
+ "nickname": "挑战创建者",
+ "avatarUrl": null,
+ "correctCount": 6,
+ "totalTimeSpent": 120
+ },
+ {
+ "rank": 3,
+ "participantId": "user_003",
+ "nickname": null,
+ "avatarUrl": null,
+ "correctCount": 5,
+ "totalTimeSpent": 60
+ }
+ ]
+ },
+ "message": null,
+ "timestamp": "2026-04-13T12:00:00.000Z"
+}
+```
+
+**业务逻辑说明**:
+
+1. 返回挑战基本信息:分享 ID、分享码、标题、关卡数量、已提交人数、当前用户排名和创建时间。
+2. `rankings` 只包含已经调用 `POST /api/v1/share/{code}/submit` 提交整场挑战结果的用户。
+3. 排行榜按答对题数降序排列;答对题数相同时,按总耗时升序排列。
+4. 如果答对题数和总耗时都相同,服务端继续按提交时间升序、`participantId` 升序做稳定排序。
+5. `userRank` 表示当前登录用户在该挑战中的排名;当前用户尚未提交结果时为 `null`。
+
+**客户端调用场景**:
+
+- 用户打开单个挑战详情页或结算页时调用。
+- 需要展示完整排行榜时调用;列表页仍使用 `GET /api/v1/share/created`。
+
+---
+
+### 5. 获取我创建的分享挑战
获取当前登录用户创建过的分享挑战列表。
@@ -322,6 +423,10 @@ Authorization: Bearer
levelCount: number; // 关卡数量
participantCount: number; // 已提交结果的参与人数
userRank: number | null; // 当前用户在该挑战中的排名;尚未提交结果时为 null
+ firstPlaceUser: { // 当前第一名用户信息;暂无提交结果时为 null
+ nickname: string | null; // 第一名用户昵称
+ avatarUrl: string | null;// 第一名用户头像 URL
+ } | null;
createdAt: string; // 创建时间,ISO 8601 字符串
}
]
@@ -342,6 +447,10 @@ Authorization: Bearer
"levelCount": 6,
"participantCount": 8,
"userRank": 2,
+ "firstPlaceUser": {
+ "nickname": "第一名玩家",
+ "avatarUrl": "https://example.com/avatar-first.png"
+ },
"createdAt": "2026-04-13T10:00:00.000Z"
},
{
@@ -351,6 +460,10 @@ Authorization: Bearer
"levelCount": 6,
"participantCount": 1,
"userRank": null,
+ "firstPlaceUser": {
+ "nickname": null,
+ "avatarUrl": null
+ },
"createdAt": "2026-04-12T09:00:00.000Z"
}
]
@@ -366,6 +479,7 @@ Authorization: Bearer
2. 排名按答对题数降序计算,答对越多排名越高。
3. 答对题数相同时,按总耗时升序、提交时间升序、`participantId` 升序做稳定排序。
4. `userRank` 表示当前登录用户在自己创建的该挑战中的排名。如果自己尚未提交挑战结果,则返回 `null`。
+5. `firstPlaceUser` 取当前排名第一的已提交用户资料。如果该挑战还没有任何提交结果,则返回 `null`。
**参与人数统计规则**:
@@ -379,7 +493,7 @@ Authorization: Bearer
---
-### 5. 提交分享挑战结果
+### 6. 提交分享挑战结果
用户完成分享挑战后,一次性提交分享中每一关的耗时和答案。服务端校验答案、持久化结果,并返回当前用户的排名和完整关卡答案。
@@ -803,7 +917,56 @@ const shareData = await joinShare(shareCode);
// 保存关卡数据,开始游戏
```
-### 5. 提交挑战结果
+### 5. 获取挑战详情
+
+```typescript
+interface ChallengeRankingItem {
+ rank: number;
+ participantId: string;
+ nickname: string | null;
+ avatarUrl: string | null;
+ correctCount: number;
+ totalTimeSpent: number;
+}
+
+interface ShareChallengeDetailResponse {
+ id: string;
+ shareCode: string;
+ title: string;
+ levelCount: number;
+ participantCount: number;
+ userRank: number | null;
+ createdAt: string;
+ rankings: ChallengeRankingItem[];
+}
+
+async function getShareChallengeDetail(
+ shareCode: string,
+): Promise {
+ // 确保已登录
+ if (!httpManager.getToken()) {
+ await wxLogin();
+ }
+
+ const response = await httpManager.get(
+ `/v1/share/${shareCode}`,
+ );
+
+ if (response.success && response.data) {
+ console.log('挑战详情:', response.data);
+ return response.data;
+ } else {
+ throw new Error(response.message || '获取挑战详情失败');
+ }
+}
+
+// 使用示例
+const detail = await getShareChallengeDetail('abc12345');
+console.log(`当前排名: ${detail.userRank ?? '暂未上榜'}`);
+console.log(`已提交人数: ${detail.participantCount}`);
+```
+
+### 6. 提交挑战结果
```typescript
interface SubmitChallengeLevel {
@@ -870,7 +1033,7 @@ async function onChallengeFinished() {
}
```
-### 6. 启动流程示例
+### 7. 启动流程示例
```typescript
// GameEntry.ts - 游戏入口脚本
diff --git a/src/modules/share/dto/share-response.dto.ts b/src/modules/share/dto/share-response.dto.ts
index aaa0ed5..2eb92ea 100644
--- a/src/modules/share/dto/share-response.dto.ts
+++ b/src/modules/share/dto/share-response.dto.ts
@@ -60,6 +60,58 @@ export class JoinShareResponseDto {
levels!: ShareLevelDto[];
}
+export class ShareChallengeRankingItemDto {
+ @ApiProperty({ description: '排名,从 1 开始' })
+ rank!: number;
+
+ @ApiProperty({ description: '参与用户 ID' })
+ participantId!: string;
+
+ @ApiProperty({ description: '参与用户昵称', nullable: true })
+ nickname!: string | null;
+
+ @ApiProperty({ description: '参与用户头像 URL', nullable: true })
+ avatarUrl!: string | null;
+
+ @ApiProperty({ description: '答对题数' })
+ correctCount!: number;
+
+ @ApiProperty({ description: '总耗时(秒)' })
+ totalTimeSpent!: number;
+}
+
+export class ShareChallengeDetailResponseDto {
+ @ApiProperty({ description: '分享 ID' })
+ id!: string;
+
+ @ApiProperty({ description: '分享码' })
+ shareCode!: string;
+
+ @ApiProperty({ description: '分享标题' })
+ title!: string;
+
+ @ApiProperty({ description: '挑战关卡总数' })
+ levelCount!: number;
+
+ @ApiProperty({ description: '已提交结果的参与用户总数' })
+ participantCount!: number;
+
+ @ApiProperty({
+ description: '当前用户在该挑战中的排名,尚未提交挑战结果时为 null',
+ nullable: true,
+ })
+ userRank!: number | null;
+
+ @ApiProperty({ description: '创建时间' })
+ createdAt!: string;
+
+ @ApiProperty({
+ description: '挑战排行榜,按答对题数降序、总耗时升序排列',
+ type: [ShareChallengeRankingItemDto],
+ })
+ rankings!: ShareChallengeRankingItemDto[];
+}
+
export class SubmittedShareLevelDto extends ShareLevelDto {
@ApiProperty({ description: '用户提交的答案' })
submittedAnswer!: string;
@@ -106,6 +158,14 @@ export class SubmitShareChallengeResponseDto {
levels!: SubmittedShareLevelDto[];
}
+export class FirstPlaceUserDto {
+ @ApiProperty({ description: '第一名用户昵称', nullable: true })
+ nickname!: string | null;
+
+ @ApiProperty({ description: '第一名用户头像 URL', nullable: true })
+ avatarUrl!: string | null;
+}
+
export class CreatedShareItemDto {
@ApiProperty({ description: '分享 ID' })
id!: string;
@@ -128,6 +188,13 @@ export class CreatedShareItemDto {
})
userRank!: number | null;
+ @ApiProperty({
+ description: '当前挑战第一名用户信息,暂无提交结果时为 null',
+ nullable: true,
+ type: () => FirstPlaceUserDto,
+ })
+ firstPlaceUser!: FirstPlaceUserDto | null;
+
@ApiProperty({ description: '创建时间' })
createdAt!: string;
}
diff --git a/src/modules/share/repositories/share-participant.repository.ts b/src/modules/share/repositories/share-participant.repository.ts
index c8e0669..e175e83 100644
--- a/src/modules/share/repositories/share-participant.repository.ts
+++ b/src/modules/share/repositories/share-participant.repository.ts
@@ -14,6 +14,8 @@ export type ShareParticipantRankingRow = {
correctCount: number;
totalTimeSpent: number;
submittedAt: Date;
+ nickname?: string | null;
+ avatarUrl?: string | null;
};
@Injectable()
@@ -120,6 +122,12 @@ export class ShareParticipantRepository {
): Promise {
const rows = await this.repository
.createQueryBuilder('participant')
+ .leftJoin('participant.participant', 'rankingUser')
+ .addSelect([
+ 'rankingUser.id',
+ 'rankingUser.nickname',
+ 'rankingUser.avatarUrl',
+ ])
.where('participant.shareConfigId = :shareConfigId', { shareConfigId })
.andWhere('participant.submittedAt IS NOT NULL')
.orderBy('participant.correctCount', 'DESC')
@@ -134,6 +142,8 @@ export class ShareParticipantRepository {
correctCount: row.correctCount,
totalTimeSpent: row.totalTimeSpent,
submittedAt: row.submittedAt!,
+ nickname: row.participant?.nickname ?? null,
+ avatarUrl: row.participant?.avatarUrl ?? null,
}));
}
@@ -146,6 +156,12 @@ export class ShareParticipantRepository {
const rows = await this.repository
.createQueryBuilder('participant')
+ .leftJoin('participant.participant', 'rankingUser')
+ .addSelect([
+ 'rankingUser.id',
+ 'rankingUser.nickname',
+ 'rankingUser.avatarUrl',
+ ])
.where('participant.shareConfigId IN (:...shareConfigIds)', {
shareConfigIds,
})
@@ -163,6 +179,8 @@ export class ShareParticipantRepository {
correctCount: row.correctCount,
totalTimeSpent: row.totalTimeSpent,
submittedAt: row.submittedAt!,
+ nickname: row.participant?.nickname ?? null,
+ avatarUrl: row.participant?.avatarUrl ?? null,
}));
}
diff --git a/src/modules/share/share.controller.spec.ts b/src/modules/share/share.controller.spec.ts
index 2e0fcde..b7fec13 100644
--- a/src/modules/share/share.controller.spec.ts
+++ b/src/modules/share/share.controller.spec.ts
@@ -16,6 +16,7 @@ describe('ShareController', () => {
const mockShareService = {
createShare: jest.fn(),
getCreatedShares: jest.fn(),
+ getShareChallengeDetail: jest.fn(),
joinShare: jest.fn(),
submitChallenge: jest.fn(),
};
@@ -72,6 +73,10 @@ describe('ShareController', () => {
levelCount: 6,
participantCount: 8,
userRank: 2,
+ firstPlaceUser: {
+ nickname: '第一名玩家',
+ avatarUrl: 'https://example.com/avatar-first.png',
+ },
createdAt: '2026-01-01T00:00:00.000Z',
},
],
@@ -90,6 +95,53 @@ describe('ShareController', () => {
});
});
+ describe('getShareChallengeDetail', () => {
+ it('should return success response with challenge detail and rankings', async () => {
+ const detailResponse = {
+ id: 'share-uuid-1',
+ shareCode: 'ABCD1234',
+ title: '我的挑战',
+ levelCount: 6,
+ participantCount: 2,
+ userRank: 2,
+ createdAt: '2026-01-01T00:00:00.000Z',
+ rankings: [
+ {
+ rank: 1,
+ participantId: 'user-uuid-2',
+ nickname: '第一名玩家',
+ avatarUrl: 'https://example.com/avatar-first.png',
+ correctCount: 6,
+ totalTimeSpent: 98,
+ },
+ {
+ rank: 2,
+ participantId: 'user-uuid-1',
+ nickname: '挑战创建者',
+ avatarUrl: null,
+ correctCount: 5,
+ totalTimeSpent: 120,
+ },
+ ],
+ };
+ mockShareService.getShareChallengeDetail.mockResolvedValue(
+ detailResponse,
+ );
+
+ const result = await controller.getShareChallengeDetail(
+ mockUser,
+ 'ABCD1234',
+ );
+
+ expect(result.success).toBe(true);
+ expect(result.data).toEqual(detailResponse);
+ expect(mockShareService.getShareChallengeDetail).toHaveBeenCalledWith(
+ 'user-uuid-1',
+ 'ABCD1234',
+ );
+ });
+ });
+
describe('joinShare', () => {
it('should return success response with share levels', async () => {
const joinResponse = {
diff --git a/src/modules/share/share.controller.ts b/src/modules/share/share.controller.ts
index b732299..c106f08 100644
--- a/src/modules/share/share.controller.ts
+++ b/src/modules/share/share.controller.ts
@@ -11,6 +11,7 @@ import {
CreateShareResponseDto,
CreatedShareListResponseDto,
JoinShareResponseDto,
+ ShareChallengeDetailResponseDto,
SubmitShareChallengeResponseDto,
} from './dto/share-response.dto';
import { ApiResponseDto } from '../../common/dto/api-response.dto';
@@ -56,6 +57,27 @@ export class ShareController {
return ApiResponseDto.success(data);
}
+ @Get(':code')
+ @UseGuards(JwtAuthGuard)
+ @ApiBearerAuth()
+ @ApiOperation({
+ summary: '获取分享挑战详情',
+ description:
+ '返回分享挑战基本信息,以及所有已提交挑战结果用户的排行榜。排行榜按答对题数降序、总耗时升序排列。',
+ })
+ @ApiResponse({ status: 200, description: '成功' })
+ @ApiResponse({ status: 404, description: '分享不存在' })
+ async getShareChallengeDetail(
+ @CurrentUser() user: JwtPayload,
+ @Param('code') code: string,
+ ): Promise> {
+ const data = await this.shareService.getShareChallengeDetail(
+ user.sub,
+ code,
+ );
+ return ApiResponseDto.success(data);
+ }
+
@Post(':code/join')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
diff --git a/src/modules/share/share.service.spec.ts b/src/modules/share/share.service.spec.ts
index 48eee77..871d21f 100644
--- a/src/modules/share/share.service.spec.ts
+++ b/src/modules/share/share.service.spec.ts
@@ -255,6 +255,8 @@ describe('ShareService', () => {
correctCount: 6,
totalTimeSpent: 100,
submittedAt: new Date('2026-01-01T00:02:00.000Z'),
+ nickname: '第一名玩家',
+ avatarUrl: 'https://example.com/avatar-first.png',
},
{
shareConfigId: 'share-uuid-1',
@@ -262,6 +264,8 @@ describe('ShareService', () => {
correctCount: 6,
totalTimeSpent: 120,
submittedAt: new Date('2026-01-01T00:01:00.000Z'),
+ nickname: '挑战创建者',
+ avatarUrl: null,
},
{
shareConfigId: 'share-uuid-1',
@@ -269,6 +273,8 @@ describe('ShareService', () => {
correctCount: 5,
totalTimeSpent: 200,
submittedAt: new Date('2026-01-01T00:03:00.000Z'),
+ nickname: null,
+ avatarUrl: null,
},
],
);
@@ -284,6 +290,7 @@ describe('ShareService', () => {
levelCount: 6,
participantCount: 1,
userRank: null,
+ firstPlaceUser: null,
createdAt: '2026-01-02T00:00:00.000Z',
},
{
@@ -293,6 +300,10 @@ describe('ShareService', () => {
levelCount: 6,
participantCount: 3,
userRank: 2,
+ firstPlaceUser: {
+ nickname: '第一名玩家',
+ avatarUrl: 'https://example.com/avatar-first.png',
+ },
createdAt: '2026-01-01T00:00:00.000Z',
},
],
@@ -331,6 +342,124 @@ describe('ShareService', () => {
});
});
+ describe('getShareChallengeDetail', () => {
+ it('should return challenge detail with all submitted rankings', async () => {
+ mockShareConfigRepository.findByShareCode.mockResolvedValue(
+ mockShareConfig,
+ );
+ mockShareParticipantRepository.findSubmittedRankingsByShareConfigId.mockResolvedValue(
+ [
+ {
+ shareConfigId: 'share-uuid-1',
+ participantId: 'user-uuid-3',
+ correctCount: 6,
+ totalTimeSpent: 90,
+ submittedAt: new Date('2026-01-01T00:03:00.000Z'),
+ nickname: '速度玩家',
+ avatarUrl: 'https://example.com/avatar-speed.png',
+ },
+ {
+ shareConfigId: 'share-uuid-1',
+ participantId: 'user-uuid-1',
+ correctCount: 6,
+ totalTimeSpent: 120,
+ submittedAt: new Date('2026-01-01T00:02:00.000Z'),
+ nickname: '挑战创建者',
+ avatarUrl: null,
+ },
+ {
+ shareConfigId: 'share-uuid-1',
+ participantId: 'user-uuid-2',
+ correctCount: 5,
+ totalTimeSpent: 60,
+ submittedAt: new Date('2026-01-01T00:01:00.000Z'),
+ nickname: null,
+ avatarUrl: null,
+ },
+ ],
+ );
+
+ const result = await service.getShareChallengeDetail(
+ 'user-uuid-1',
+ 'ABCD1234',
+ );
+
+ expect(result).toEqual({
+ id: 'share-uuid-1',
+ shareCode: 'ABCD1234',
+ title: '我的挑战',
+ levelCount: 6,
+ participantCount: 3,
+ userRank: 2,
+ createdAt: '2026-01-01T00:00:00.000Z',
+ rankings: [
+ {
+ rank: 1,
+ participantId: 'user-uuid-3',
+ nickname: '速度玩家',
+ avatarUrl: 'https://example.com/avatar-speed.png',
+ correctCount: 6,
+ totalTimeSpent: 90,
+ },
+ {
+ rank: 2,
+ participantId: 'user-uuid-1',
+ nickname: '挑战创建者',
+ avatarUrl: null,
+ correctCount: 6,
+ totalTimeSpent: 120,
+ },
+ {
+ rank: 3,
+ participantId: 'user-uuid-2',
+ nickname: null,
+ avatarUrl: null,
+ correctCount: 5,
+ totalTimeSpent: 60,
+ },
+ ],
+ });
+ expect(
+ mockShareParticipantRepository.findSubmittedRankingsByShareConfigId,
+ ).toHaveBeenCalledWith('share-uuid-1');
+ });
+
+ it('should return null userRank when current user has not submitted', async () => {
+ mockShareConfigRepository.findByShareCode.mockResolvedValue(
+ mockShareConfig,
+ );
+ mockShareParticipantRepository.findSubmittedRankingsByShareConfigId.mockResolvedValue(
+ [
+ {
+ shareConfigId: 'share-uuid-1',
+ participantId: 'user-uuid-2',
+ correctCount: 5,
+ totalTimeSpent: 80,
+ submittedAt: new Date('2026-01-01T00:01:00.000Z'),
+ nickname: null,
+ avatarUrl: null,
+ },
+ ],
+ );
+
+ const result = await service.getShareChallengeDetail(
+ 'user-uuid-1',
+ 'ABCD1234',
+ );
+
+ expect(result.userRank).toBeNull();
+ expect(result.participantCount).toBe(1);
+ });
+
+ it('should throw NotFoundException when share code not found', async () => {
+ mockShareConfigRepository.findByShareCode.mockResolvedValue(null);
+
+ await expect(
+ service.getShareChallengeDetail('user-uuid-1', 'INVALID'),
+ ).rejects.toThrow(NotFoundException);
+ });
+ });
+
describe('submitChallenge', () => {
const submitDto = {
levels: mockLevels.map((level, index) => ({
diff --git a/src/modules/share/share.service.ts b/src/modules/share/share.service.ts
index a5a3eee..564a42f 100644
--- a/src/modules/share/share.service.ts
+++ b/src/modules/share/share.service.ts
@@ -16,6 +16,7 @@ import {
CreateShareResponseDto,
CreatedShareListResponseDto,
JoinShareResponseDto,
+ ShareChallengeDetailResponseDto,
ShareLevelDto,
SubmittedShareLevelDto,
SubmitShareChallengeResponseDto,
@@ -95,6 +96,43 @@ export class ShareService {
};
}
+ async getShareChallengeDetail(
+ userId: string,
+ code: string,
+ ): Promise {
+ const config = await this.shareConfigRepository.findByShareCode(code);
+ if (!config) {
+ throw new NotFoundException('分享不存在或已过期');
+ }
+
+ const rankings =
+ await this.shareParticipantRepository.findSubmittedRankingsByShareConfigId(
+ config.id,
+ );
+ const rankingItems = rankings.map((ranking, index) => ({
+ rank: index + 1,
+ participantId: ranking.participantId,
+ nickname: ranking.nickname ?? null,
+ avatarUrl: ranking.avatarUrl ?? null,
+ correctCount: ranking.correctCount,
+ totalTimeSpent: ranking.totalTimeSpent,
+ }));
+ const userRanking = rankingItems.find(
+ (ranking) => ranking.participantId === userId,
+ );
+
+ return {
+ id: config.id,
+ shareCode: config.shareCode,
+ title: config.title,
+ levelCount: config.levelIds.length,
+ participantCount: rankingItems.length,
+ userRank: userRanking?.rank ?? null,
+ createdAt: config.createdAt.toISOString(),
+ rankings: rankingItems,
+ };
+ }
+
async getCreatedShares(userId: string): Promise {
const configs = await this.shareConfigRepository.findBySharerId(userId);
if (configs.length === 0) {
@@ -111,11 +149,11 @@ export class ShareService {
),
]);
- const rankingsByShareConfigId = new Map();
+ const rankingsByShareConfigId = new Map();
for (const config of configs) {
- const rankings = rankingRows
- .filter((row) => row.shareConfigId === config.id)
- .map((row) => row.participantId);
+ const rankings = rankingRows.filter(
+ (row) => row.shareConfigId === config.id,
+ );
rankingsByShareConfigId.set(config.id, rankings);
}
@@ -124,8 +162,9 @@ export class ShareService {
items: configs.map((config) => {
const rankings = rankingsByShareConfigId.get(config.id) ?? [];
const rankingIndex = rankings.findIndex(
- (participantId) => participantId === userId,
+ (ranking) => ranking.participantId === userId,
);
+ const firstPlaceRanking = rankings[0];
return {
id: config.id,
@@ -134,6 +173,12 @@ export class ShareService {
levelCount: config.levelIds.length,
participantCount: participantCountMap.get(config.id) ?? 0,
userRank: rankingIndex >= 0 ? rankingIndex + 1 : null,
+ firstPlaceUser: firstPlaceRanking
+ ? {
+ nickname: firstPlaceRanking.nickname ?? null,
+ avatarUrl: firstPlaceRanking.avatarUrl ?? null,
+ }
+ : null,
createdAt: config.createdAt.toISOString(),
};
}),