fix: 修复创建分享接口报错
This commit is contained in:
@@ -422,12 +422,14 @@ Authorization: Bearer <token>
|
|||||||
|
|
||||||
**业务逻辑说明**:
|
**业务逻辑说明**:
|
||||||
|
|
||||||
1. **首次通关记录**:只有首次 `passed=true` 才会记录通关时间
|
1. **参与者登记**:非创建者首次上报进度时会自动写入 `share_participants`,后续重复上报已存在则忽略;创建者本人不会被登记为参与者。
|
||||||
2. **重复通关**:如果用户再次通关同一关卡(且之前已通过),返回之前记录的时间判断结果,不会覆盖
|
2. **首次通关记录**:只有首次 `passed=true` 才会记录通关时间
|
||||||
3. **未通过**:可以多次上报 `passed=false`,更新通关时间记录
|
3. **重复通关**:如果用户再次通关同一关卡(且之前已通过),返回之前记录的时间判断结果,不会覆盖
|
||||||
4. **时间限制判断**:
|
4. **未通过**:可以多次上报 `passed=false`,更新通关时间记录
|
||||||
|
5. **时间限制判断**:
|
||||||
- 如果关卡 `timeLimit` 为 `null`,`withinTimeLimit` 始终为 `true`
|
- 如果关卡 `timeLimit` 为 `null`,`withinTimeLimit` 始终为 `true`
|
||||||
- 如果 `timeLimit` 不为 `null`,只有 `timeSpent <= timeLimit` 时 `withinTimeLimit` 才为 `true`
|
- 如果 `timeLimit` 不为 `null`,只有 `timeSpent <= timeLimit` 时 `withinTimeLimit` 才为 `true`
|
||||||
|
6. **跨挑战独立**:进度按 `(shareConfigId, participantId, levelId)` 唯一记录,同一用户在不同分享挑战中对同一关卡的进度互不影响。
|
||||||
|
|
||||||
**客户端调用场景**:
|
**客户端调用场景**:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- Description: Align share_participants and share_level_progress with the
|
||||||
|
-- "share_participants is a pure association table without an id" model.
|
||||||
|
--
|
||||||
|
-- Background: the production share_participants table has no `id` column —
|
||||||
|
-- its primary key is the (share_config_id, participant_id) pair. The earlier
|
||||||
|
-- ShareParticipant entity declared a generated `id` column, which broke any
|
||||||
|
-- query that referenced participant.id (e.g. COUNT(participant.id)) with
|
||||||
|
-- "Unknown column 'participant.id' in 'field list'".
|
||||||
|
--
|
||||||
|
-- This migration:
|
||||||
|
-- 1. Promotes (share_config_id, participant_id) to the primary key on
|
||||||
|
-- share_participants and drops the now-redundant unique key.
|
||||||
|
-- 2. Replaces share_level_progress.participant_id (a pointer to
|
||||||
|
-- share_participants.id) with the natural pair
|
||||||
|
-- (share_config_id, participant_id) where participant_id stores the user
|
||||||
|
-- UUID directly. This way a single user's progress in different share
|
||||||
|
-- challenges no longer collides on the same level.
|
||||||
|
--
|
||||||
|
-- IMPORTANT: step 2 backfills participant_id by joining the existing
|
||||||
|
-- share_participants rows. Run on a maintenance window if there is real data.
|
||||||
|
|
||||||
|
-- 1. share_participants: drop redundant unique key, set composite PK.
|
||||||
|
ALTER TABLE share_participants
|
||||||
|
DROP INDEX uq_share_participant;
|
||||||
|
|
||||||
|
ALTER TABLE share_participants
|
||||||
|
ADD PRIMARY KEY (share_config_id, participant_id);
|
||||||
|
|
||||||
|
-- 2. share_level_progress: introduce share_config_id + remap participant_id.
|
||||||
|
ALTER TABLE share_level_progress
|
||||||
|
ADD COLUMN share_config_id VARCHAR(191) NULL AFTER id;
|
||||||
|
|
||||||
|
-- Backfill share_config_id and rewrite participant_id from the user UUID.
|
||||||
|
-- (Old participant_id referenced share_participants.id; new schema stores the
|
||||||
|
-- user UUID directly.)
|
||||||
|
UPDATE share_level_progress slp
|
||||||
|
INNER JOIN share_participants sp
|
||||||
|
ON sp.id = slp.participant_id
|
||||||
|
SET
|
||||||
|
slp.share_config_id = sp.share_config_id,
|
||||||
|
slp.participant_id = sp.participant_id;
|
||||||
|
|
||||||
|
ALTER TABLE share_level_progress
|
||||||
|
MODIFY COLUMN share_config_id VARCHAR(191) NOT NULL,
|
||||||
|
MODIFY COLUMN participant_id VARCHAR(191) NOT NULL;
|
||||||
|
|
||||||
|
-- Drop the legacy FK / unique key that pointed at share_participants.id.
|
||||||
|
-- The FK was auto-named by MySQL when migration 002 ran. If the name below
|
||||||
|
-- differs in your environment, look it up:
|
||||||
|
-- SELECT constraint_name FROM information_schema.referential_constraints
|
||||||
|
-- WHERE table_name = 'share_level_progress';
|
||||||
|
ALTER TABLE share_level_progress
|
||||||
|
DROP FOREIGN KEY share_level_progress_ibfk_1;
|
||||||
|
|
||||||
|
ALTER TABLE share_level_progress
|
||||||
|
DROP INDEX uq_participant_level;
|
||||||
|
|
||||||
|
ALTER TABLE share_level_progress
|
||||||
|
ADD UNIQUE KEY uq_share_participant_level
|
||||||
|
(share_config_id, participant_id, level_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_slp_share_config_id
|
||||||
|
ON share_level_progress (share_config_id);
|
||||||
|
|
||||||
|
ALTER TABLE share_level_progress
|
||||||
|
ADD CONSTRAINT fk_slp_share_config
|
||||||
|
FOREIGN KEY (share_config_id) REFERENCES share_configs(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
@@ -7,22 +7,38 @@ import {
|
|||||||
Index,
|
Index,
|
||||||
Unique,
|
Unique,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { ShareParticipant } from './share-participant.entity';
|
import { ShareConfig } from './share-config.entity';
|
||||||
import { Level } from '../../wechat-game/entities/level.entity';
|
import { Level } from '../../wechat-game/entities/level.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分享挑战内的单关进度。
|
||||||
|
*
|
||||||
|
* (share_config_id, participant_id, level_id) 三元组保证:
|
||||||
|
* 同一用户在不同分享挑战中对同一关的记录互不干扰。
|
||||||
|
*
|
||||||
|
* participant_id 直接存储 wx_users.id(用户 UUID),不再引用 share_participants。
|
||||||
|
*/
|
||||||
@Entity('share_level_progress')
|
@Entity('share_level_progress')
|
||||||
@Unique('uq_participant_level', ['participantId', 'levelId'])
|
@Unique('uq_share_participant_level', [
|
||||||
|
'shareConfigId',
|
||||||
|
'participantId',
|
||||||
|
'levelId',
|
||||||
|
])
|
||||||
export class ShareLevelProgress {
|
export class ShareLevelProgress {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Index('idx_slp_participant_id')
|
@Index('idx_slp_share_config_id')
|
||||||
@Column({ type: 'char', length: 36, name: 'participant_id' })
|
@Column({ type: 'varchar', length: 191, name: 'share_config_id' })
|
||||||
participantId!: string;
|
shareConfigId!: string;
|
||||||
|
|
||||||
@ManyToOne(() => ShareParticipant)
|
@ManyToOne(() => ShareConfig)
|
||||||
@JoinColumn({ name: 'participant_id' })
|
@JoinColumn({ name: 'share_config_id' })
|
||||||
participant!: ShareParticipant;
|
shareConfig!: ShareConfig;
|
||||||
|
|
||||||
|
@Index('idx_slp_participant_id')
|
||||||
|
@Column({ type: 'varchar', length: 191, name: 'participant_id' })
|
||||||
|
participantId!: string;
|
||||||
|
|
||||||
@Index('idx_slp_level_id')
|
@Index('idx_slp_level_id')
|
||||||
@Column({ type: 'char', length: 191, name: 'level_id' })
|
@Column({ type: 'char', length: 191, name: 'level_id' })
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryColumn,
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
Index,
|
Index,
|
||||||
Unique,
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../../auth/entities/user.entity';
|
import { User } from '../../auth/entities/user.entity';
|
||||||
import { ShareConfig } from './share-config.entity';
|
import { ShareConfig } from './share-config.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分享挑战参与者关联表。
|
||||||
|
*
|
||||||
|
* 该表为纯关联表,没有独立主键 id:
|
||||||
|
* - 主键由 (share_config_id, participant_id) 组成
|
||||||
|
* - participant_id 直接存储 wx_users.id(用户 UUID)
|
||||||
|
*/
|
||||||
@Entity('share_participants')
|
@Entity('share_participants')
|
||||||
@Unique('uq_share_participant', ['shareConfigId', 'participantId'])
|
|
||||||
export class ShareParticipant {
|
export class ShareParticipant {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryColumn({ type: 'varchar', length: 191, name: 'share_config_id' })
|
||||||
id!: string;
|
|
||||||
|
|
||||||
@Index('idx_share_config_id')
|
@Index('idx_share_config_id')
|
||||||
@Column({ type: 'varchar', length: 191, name: 'share_config_id' })
|
|
||||||
shareConfigId!: string;
|
shareConfigId!: string;
|
||||||
|
|
||||||
|
@PrimaryColumn({ type: 'varchar', length: 191, name: 'participant_id' })
|
||||||
|
participantId!: string;
|
||||||
|
|
||||||
@ManyToOne(() => ShareConfig, (sc) => sc.participants)
|
@ManyToOne(() => ShareConfig, (sc) => sc.participants)
|
||||||
@JoinColumn({ name: 'share_config_id' })
|
@JoinColumn({ name: 'share_config_id' })
|
||||||
shareConfig!: ShareConfig;
|
shareConfig!: ShareConfig;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 191, name: 'participant_id' })
|
|
||||||
participantId!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
@ManyToOne(() => User)
|
||||||
@JoinColumn({ name: 'participant_id' })
|
@JoinColumn({ name: 'participant_id' })
|
||||||
participant!: User;
|
participant!: User;
|
||||||
|
|||||||
@@ -17,17 +17,21 @@ export class ShareLevelProgressRepository {
|
|||||||
private readonly repository: Repository<ShareLevelProgress>,
|
private readonly repository: Repository<ShareLevelProgress>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findByParticipantId(
|
async findByShareConfigAndParticipant(
|
||||||
|
shareConfigId: string,
|
||||||
participantId: string,
|
participantId: string,
|
||||||
): Promise<ShareLevelProgress[]> {
|
): Promise<ShareLevelProgress[]> {
|
||||||
return this.repository.find({ where: { participantId } });
|
return this.repository.find({ where: { shareConfigId, participantId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByParticipantAndLevel(
|
async findByShareConfigParticipantAndLevel(
|
||||||
|
shareConfigId: string,
|
||||||
participantId: string,
|
participantId: string,
|
||||||
levelId: string,
|
levelId: string,
|
||||||
): Promise<ShareLevelProgress | null> {
|
): Promise<ShareLevelProgress | null> {
|
||||||
return this.repository.findOne({ where: { participantId, levelId } });
|
return this.repository.findOne({
|
||||||
|
where: { shareConfigId, participantId, levelId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async summarizeByShareConfigIds(
|
async summarizeByShareConfigIds(
|
||||||
@@ -39,17 +43,16 @@ export class ShareLevelProgressRepository {
|
|||||||
|
|
||||||
return this.repository
|
return this.repository
|
||||||
.createQueryBuilder('progress')
|
.createQueryBuilder('progress')
|
||||||
.innerJoin('progress.participant', 'participant')
|
.select('progress.shareConfigId', 'shareConfigId')
|
||||||
.select('participant.shareConfigId', 'shareConfigId')
|
.addSelect('progress.participantId', 'participantId')
|
||||||
.addSelect('participant.participantId', 'participantId')
|
|
||||||
.addSelect('SUM(progress.timeSpent)', 'totalTimeSpent')
|
.addSelect('SUM(progress.timeSpent)', 'totalTimeSpent')
|
||||||
.addSelect('COUNT(DISTINCT progress.levelId)', 'passedLevelCount')
|
.addSelect('COUNT(DISTINCT progress.levelId)', 'passedLevelCount')
|
||||||
.where('participant.shareConfigId IN (:...shareConfigIds)', {
|
.where('progress.shareConfigId IN (:...shareConfigIds)', {
|
||||||
shareConfigIds,
|
shareConfigIds,
|
||||||
})
|
})
|
||||||
.andWhere('progress.passed = :passed', { passed: true })
|
.andWhere('progress.passed = :passed', { passed: true })
|
||||||
.groupBy('participant.shareConfigId')
|
.groupBy('progress.shareConfigId')
|
||||||
.addGroupBy('participant.participantId')
|
.addGroupBy('progress.participantId')
|
||||||
.getRawMany<ShareChallengeRankingRow>();
|
.getRawMany<ShareChallengeRankingRow>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class ShareParticipantRepository {
|
|||||||
const rows = await this.repository
|
const rows = await this.repository
|
||||||
.createQueryBuilder('participant')
|
.createQueryBuilder('participant')
|
||||||
.select('participant.shareConfigId', 'shareConfigId')
|
.select('participant.shareConfigId', 'shareConfigId')
|
||||||
.addSelect('COUNT(participant.id)', 'participantCount')
|
.addSelect('COUNT(*)', 'participantCount')
|
||||||
.where('participant.shareConfigId IN (:...shareConfigIds)', {
|
.where('participant.shareConfigId IN (:...shareConfigIds)', {
|
||||||
shareConfigIds,
|
shareConfigIds,
|
||||||
})
|
})
|
||||||
@@ -55,18 +55,13 @@ export class ShareParticipantRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByShareConfigAndParticipant(
|
async existsByShareConfigAndParticipant(
|
||||||
shareConfigId: string,
|
shareConfigId: string,
|
||||||
participantId: string,
|
participantId: string,
|
||||||
): Promise<ShareParticipant | null> {
|
): Promise<boolean> {
|
||||||
return this.repository.findOne({ where: { shareConfigId, participantId } });
|
const count = await this.repository.count({
|
||||||
}
|
where: { shareConfigId, participantId },
|
||||||
|
});
|
||||||
create(data: Partial<ShareParticipant>): ShareParticipant {
|
return count > 0;
|
||||||
return this.repository.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(participant: ShareParticipant): Promise<ShareParticipant> {
|
|
||||||
return this.repository.save(participant);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ShareLevelProgressRepository } from './repositories/share-level-progres
|
|||||||
import { LevelRepository } from '../wechat-game/repositories/level.repository';
|
import { LevelRepository } from '../wechat-game/repositories/level.repository';
|
||||||
import { Level } from '../wechat-game/entities/level.entity';
|
import { Level } from '../wechat-game/entities/level.entity';
|
||||||
import { ShareConfig } from './entities/share-config.entity';
|
import { ShareConfig } from './entities/share-config.entity';
|
||||||
import { ShareParticipant } from './entities/share-participant.entity';
|
|
||||||
import { ShareLevelProgress } from './entities/share-level-progress.entity';
|
import { ShareLevelProgress } from './entities/share-level-progress.entity';
|
||||||
import { User } from '../auth/entities/user.entity';
|
import { User } from '../auth/entities/user.entity';
|
||||||
|
|
||||||
@@ -49,15 +48,6 @@ describe('ShareService', () => {
|
|||||||
updatedAt: new Date('2026-01-01'),
|
updatedAt: new Date('2026-01-01'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockParticipant: ShareParticipant = {
|
|
||||||
id: 'participant-uuid-1',
|
|
||||||
shareConfigId: 'share-uuid-1',
|
|
||||||
participantId: 'user-uuid-2',
|
|
||||||
shareConfig: {} as ShareConfig,
|
|
||||||
participant: {} as User,
|
|
||||||
createdAt: new Date('2026-01-01'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockShareConfigRepository = {
|
const mockShareConfigRepository = {
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
findByShareCode: jest.fn(),
|
findByShareCode: jest.fn(),
|
||||||
@@ -68,13 +58,11 @@ describe('ShareService', () => {
|
|||||||
addParticipant: jest.fn(),
|
addParticipant: jest.fn(),
|
||||||
countByShareConfigId: jest.fn(),
|
countByShareConfigId: jest.fn(),
|
||||||
countByShareConfigIds: jest.fn(),
|
countByShareConfigIds: jest.fn(),
|
||||||
findByShareConfigAndParticipant: jest.fn(),
|
existsByShareConfigAndParticipant: jest.fn(),
|
||||||
create: jest.fn(),
|
|
||||||
save: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockShareLevelProgressRepository = {
|
const mockShareLevelProgressRepository = {
|
||||||
findByParticipantAndLevel: jest.fn(),
|
findByShareConfigParticipantAndLevel: jest.fn(),
|
||||||
summarizeByShareConfigIds: jest.fn(),
|
summarizeByShareConfigIds: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
save: jest.fn(),
|
save: jest.fn(),
|
||||||
@@ -345,14 +333,15 @@ describe('ShareService', () => {
|
|||||||
mockShareConfig,
|
mockShareConfig,
|
||||||
);
|
);
|
||||||
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
||||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
mockShareParticipantRepository.addParticipant.mockResolvedValue(
|
||||||
mockParticipant,
|
undefined,
|
||||||
);
|
);
|
||||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel.mockResolvedValue(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const newProgress: Partial<ShareLevelProgress> = {
|
const newProgress: Partial<ShareLevelProgress> = {
|
||||||
participantId: 'participant-uuid-1',
|
shareConfigId: 'share-uuid-1',
|
||||||
|
participantId: 'user-uuid-2',
|
||||||
levelId: 'level-1',
|
levelId: 'level-1',
|
||||||
passed: true,
|
passed: true,
|
||||||
timeSpent: 30,
|
timeSpent: 30,
|
||||||
@@ -368,6 +357,30 @@ describe('ShareService', () => {
|
|||||||
expect(result.passed).toBe(true);
|
expect(result.passed).toBe(true);
|
||||||
expect(result.timeLimit).toBe(60);
|
expect(result.timeLimit).toBe(60);
|
||||||
expect(result.withinTimeLimit).toBe(true);
|
expect(result.withinTimeLimit).toBe(true);
|
||||||
|
expect(
|
||||||
|
mockShareParticipantRepository.addParticipant,
|
||||||
|
).toHaveBeenCalledWith('share-uuid-1', 'user-uuid-2');
|
||||||
|
expect(
|
||||||
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel,
|
||||||
|
).toHaveBeenCalledWith('share-uuid-1', 'user-uuid-2', 'level-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register sharer themselves as participant when reporting progress', async () => {
|
||||||
|
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||||
|
mockShareConfig,
|
||||||
|
);
|
||||||
|
mockLevelRepository.findById.mockResolvedValue(mockLevels[0]);
|
||||||
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel.mockResolvedValue(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
mockShareLevelProgressRepository.create.mockReturnValue({} as any);
|
||||||
|
mockShareLevelProgressRepository.save.mockResolvedValue({} as any);
|
||||||
|
|
||||||
|
await service.reportLevelProgress('user-uuid-1', reportDto);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockShareParticipantRepository.addParticipant,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return existing result when already passed (idempotent)', async () => {
|
it('should return existing result when already passed (idempotent)', async () => {
|
||||||
@@ -376,13 +389,11 @@ describe('ShareService', () => {
|
|||||||
mockShareConfig,
|
mockShareConfig,
|
||||||
);
|
);
|
||||||
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
||||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel.mockResolvedValue(
|
||||||
mockParticipant,
|
|
||||||
);
|
|
||||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
|
||||||
{
|
{
|
||||||
id: 'progress-uuid-1',
|
id: 'progress-uuid-1',
|
||||||
participantId: 'participant-uuid-1',
|
shareConfigId: 'share-uuid-1',
|
||||||
|
participantId: 'user-uuid-2',
|
||||||
levelId: 'level-1',
|
levelId: 'level-1',
|
||||||
passed: true,
|
passed: true,
|
||||||
timeSpent: 25,
|
timeSpent: 25,
|
||||||
@@ -406,14 +417,12 @@ describe('ShareService', () => {
|
|||||||
mockShareConfig,
|
mockShareConfig,
|
||||||
);
|
);
|
||||||
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
||||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel.mockResolvedValue(
|
||||||
mockParticipant,
|
|
||||||
);
|
|
||||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const newProgress: Partial<ShareLevelProgress> = {
|
const newProgress: Partial<ShareLevelProgress> = {
|
||||||
participantId: 'participant-uuid-1',
|
shareConfigId: 'share-uuid-1',
|
||||||
|
participantId: 'user-uuid-2',
|
||||||
levelId: 'level-1',
|
levelId: 'level-1',
|
||||||
passed: true,
|
passed: true,
|
||||||
timeSpent: 30,
|
timeSpent: 30,
|
||||||
@@ -436,14 +445,12 @@ describe('ShareService', () => {
|
|||||||
mockShareConfig,
|
mockShareConfig,
|
||||||
);
|
);
|
||||||
mockLevelRepository.findById.mockResolvedValue(levelNoTimeLimit);
|
mockLevelRepository.findById.mockResolvedValue(levelNoTimeLimit);
|
||||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel.mockResolvedValue(
|
||||||
mockParticipant,
|
|
||||||
);
|
|
||||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const newProgress: Partial<ShareLevelProgress> = {
|
const newProgress: Partial<ShareLevelProgress> = {
|
||||||
participantId: 'participant-uuid-1',
|
shareConfigId: 'share-uuid-1',
|
||||||
|
participantId: 'user-uuid-2',
|
||||||
levelId: 'level-2',
|
levelId: 'level-2',
|
||||||
passed: true,
|
passed: true,
|
||||||
timeSpent: 999,
|
timeSpent: 999,
|
||||||
@@ -461,32 +468,6 @@ describe('ShareService', () => {
|
|||||||
expect(result.withinTimeLimit).toBe(true);
|
expect(result.withinTimeLimit).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create participant if not exists when reporting progress', async () => {
|
|
||||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
|
||||||
mockShareConfig,
|
|
||||||
);
|
|
||||||
mockLevelRepository.findById.mockResolvedValue(mockLevels[0]);
|
|
||||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const createdParticipant = { ...mockParticipant };
|
|
||||||
mockShareParticipantRepository.create.mockReturnValue(createdParticipant);
|
|
||||||
mockShareParticipantRepository.save.mockResolvedValue(createdParticipant);
|
|
||||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
mockShareLevelProgressRepository.create.mockReturnValue({} as any);
|
|
||||||
mockShareLevelProgressRepository.save.mockResolvedValue({} as any);
|
|
||||||
|
|
||||||
await service.reportLevelProgress('user-uuid-2', reportDto);
|
|
||||||
|
|
||||||
expect(mockShareParticipantRepository.create).toHaveBeenCalledWith({
|
|
||||||
shareConfigId: 'share-uuid-1',
|
|
||||||
participantId: 'user-uuid-2',
|
|
||||||
});
|
|
||||||
expect(mockShareParticipantRepository.save).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw NotFoundException when share not found', async () => {
|
it('should throw NotFoundException when share not found', async () => {
|
||||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(null);
|
mockShareConfigRepository.findByShareCode.mockResolvedValue(null);
|
||||||
|
|
||||||
@@ -511,20 +492,19 @@ describe('ShareService', () => {
|
|||||||
mockShareConfig,
|
mockShareConfig,
|
||||||
);
|
);
|
||||||
mockLevelRepository.findById.mockResolvedValue(mockLevels[0]);
|
mockLevelRepository.findById.mockResolvedValue(mockLevels[0]);
|
||||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
|
||||||
mockParticipant,
|
|
||||||
);
|
|
||||||
const existingProgress = {
|
const existingProgress = {
|
||||||
id: 'progress-uuid-1',
|
id: 'progress-uuid-1',
|
||||||
participantId: 'participant-uuid-1',
|
shareConfigId: 'share-uuid-1',
|
||||||
|
participantId: 'user-uuid-2',
|
||||||
levelId: 'level-1',
|
levelId: 'level-1',
|
||||||
passed: false,
|
passed: false,
|
||||||
timeSpent: 15,
|
timeSpent: 15,
|
||||||
completedAt: new Date('2026-01-01'),
|
completedAt: new Date('2026-01-01'),
|
||||||
} as ShareLevelProgress;
|
} as ShareLevelProgress;
|
||||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
mockShareLevelProgressRepository.findByShareConfigParticipantAndLevel.mockResolvedValue(
|
||||||
existingProgress,
|
existingProgress,
|
||||||
);
|
);
|
||||||
|
mockShareLevelProgressRepository.create.mockReturnValue(existingProgress);
|
||||||
mockShareLevelProgressRepository.save.mockResolvedValue({
|
mockShareLevelProgressRepository.save.mockResolvedValue({
|
||||||
...existingProgress,
|
...existingProgress,
|
||||||
passed: true,
|
passed: true,
|
||||||
|
|||||||
@@ -185,22 +185,15 @@ export class ShareService {
|
|||||||
throw new BadRequestException('该关卡不属于此分享挑战');
|
throw new BadRequestException('该关卡不属于此分享挑战');
|
||||||
}
|
}
|
||||||
|
|
||||||
let participant =
|
// 自动登记参与者(创建者本人不计入)。已存在则忽略。
|
||||||
await this.shareParticipantRepository.findByShareConfigAndParticipant(
|
if (userId !== config.sharerId) {
|
||||||
config.id,
|
await this.shareParticipantRepository.addParticipant(config.id, userId);
|
||||||
userId,
|
|
||||||
);
|
|
||||||
if (!participant) {
|
|
||||||
participant = this.shareParticipantRepository.create({
|
|
||||||
shareConfigId: config.id,
|
|
||||||
participantId: userId,
|
|
||||||
});
|
|
||||||
participant = await this.shareParticipantRepository.save(participant);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const progress =
|
const progress =
|
||||||
await this.shareLevelProgressRepository.findByParticipantAndLevel(
|
await this.shareLevelProgressRepository.findByShareConfigParticipantAndLevel(
|
||||||
participant.id,
|
config.id,
|
||||||
|
userId,
|
||||||
dto.levelId,
|
dto.levelId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -226,7 +219,8 @@ export class ShareService {
|
|||||||
completedAt: dto.passed ? new Date() : progress.completedAt,
|
completedAt: dto.passed ? new Date() : progress.completedAt,
|
||||||
})
|
})
|
||||||
: this.shareLevelProgressRepository.create({
|
: this.shareLevelProgressRepository.create({
|
||||||
participantId: participant.id,
|
shareConfigId: config.id,
|
||||||
|
participantId: userId,
|
||||||
levelId: dto.levelId,
|
levelId: dto.levelId,
|
||||||
passed: dto.passed,
|
passed: dto.passed,
|
||||||
timeSpent: dto.timeSpent,
|
timeSpent: dto.timeSpent,
|
||||||
|
|||||||
Reference in New Issue
Block a user