feat: configure daily check-in rewards

This commit is contained in:
Codex
2026-06-30 14:52:10 +08:00
parent 7d2b8fd610
commit a5a2caa2e9
12 changed files with 610 additions and 94 deletions

View File

@@ -61,6 +61,8 @@ const ids = {
tenantPracticeBadge: '00000000-0000-0000-0000-000000000881',
tenantVocabularyBadge: '00000000-0000-0000-0000-000000000882',
tenantMockExamBadge: '00000000-0000-0000-0000-000000000883',
dailyCheckInTask: '00000000-0000-0000-0000-000000000884',
streakCheckInTask: '00000000-0000-0000-0000-000000000885',
pointActivityTask: '00000000-0000-0000-0000-000000000877',
pointSystemTask: '00000000-0000-0000-0000-000000000878',
pointExchangeItem: '00000000-0000-0000-0000-000000000879',
@@ -2878,11 +2880,69 @@ async function testProfile() {
});
assert.equal(scoreBadge.item?.id, ids.tenantScoreBadge, 'tenant admin should create automatic score badge');
const dailyCheckInTask = await request('/api/tenant-admin/point-activity-tasks', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',
body: {
id: ids.dailyCheckInTask,
legacyId: 'integration-point-task-daily-check-in',
code: 'integration-daily-check-in',
title: '每日签到基础奖励',
taskType: 'daily_check_in',
rewardPoints: 12,
periodType: 'daily',
claimLimitPerUser: 1,
metadata: { source: 'integration-test' },
order: 1,
status: 'active',
},
});
assert.equal(dailyCheckInTask.item?.id, ids.dailyCheckInTask, 'tenant admin should configure daily check-in reward task');
const streakCheckInTask = await request('/api/tenant-admin/point-activity-tasks', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',
body: {
id: ids.streakCheckInTask,
legacyId: 'integration-point-task-check-in-streak-day-one',
code: 'integration-check-in-streak-day-one',
title: '连续签到首日奖励',
taskType: 'daily_check_in',
rewardPoints: 5,
periodType: 'daily',
claimLimitPerUser: 1,
metadata: { source: 'integration-test', requiredStreak: 1 },
order: 2,
status: 'active',
},
});
assert.equal(streakCheckInTask.item?.id, ids.streakCheckInTask, 'tenant admin should configure streak check-in reward task');
const checkIn = await request('/api/profile/check-in', { method: 'POST' });
assert.equal(checkIn.item?.checkedIn, true, 'student should be able to check in');
assert.ok(checkIn.item?.pointsAdded >= 10, 'check-in should add points');
assert.equal(checkIn.item?.rewardMode, 'configured_tasks', 'check-in should use configured tenant reward tasks');
assert.equal(checkIn.item?.pointsAdded, 17, 'configured check-in tasks should add total points');
assert.ok(checkIn.item?.score >= checkIn.item?.pointsAdded, 'check-in should return updated score');
assert.equal(checkIn.item?.ledger?.eventType, 'check_in', 'check-in should write score ledger');
assert.equal(checkIn.item?.ledger?.sourceId, ids.streakCheckInTask, 'compat check-in ledger should expose the latest configured task ledger');
assert.ok(
checkIn.item?.rewardBreakdown?.some(item => item.taskId === ids.dailyCheckInTask && item.points === 12),
'check-in should include the daily task reward breakdown',
);
assert.ok(
checkIn.item?.rewardBreakdown?.some(item => item.taskId === ids.streakCheckInTask && item.points === 5),
'check-in should include the streak task reward breakdown',
);
assert.equal(checkIn.item?.ledgers?.length, 2, 'check-in should write one ledger per configured reward task');
assert.ok(
checkIn.item?.ledgers?.some(item => item.sourceId === ids.dailyCheckInTask && item.points === 12),
'check-in ledgers should include the daily task source',
);
assert.ok(
checkIn.item?.ledgers?.some(item => item.sourceId === ids.streakCheckInTask && item.points === 5),
'check-in ledgers should include the streak task source',
);
assert.equal(checkIn.item?.claims?.length, 2, 'check-in should write one claim per configured reward task');
assert.ok(
checkIn.item?.autoBadges?.some(item => item.badgeId === ids.tenantCheckInBadge),
'check-in should auto grant check-in badge',
@@ -2897,9 +2957,28 @@ async function testProfile() {
assert.equal(duplicateCheckIn.item?.alreadyCheckedIn, true, 'duplicate daily check-in should report already checked in');
assert.equal(duplicateCheckIn.item?.pointsAdded, 0, 'duplicate daily check-in should not add points');
assert.equal(duplicateCheckIn.item?.autoBadges, undefined, 'duplicate check-in should not auto grant badges');
assert.deepEqual(duplicateCheckIn.item?.rewardBreakdown, [], 'duplicate check-in should not return reward tasks');
const scoreEvents = await request('/api/profile/score-events', { query: { limit: 20 } });
assert.ok(scoreEvents.items?.some(item => item.eventType === 'check_in'), 'score ledger should include check-in event');
assert.ok(
scoreEvents.items?.some(item => item.eventType === 'check_in' && item.sourceId === ids.dailyCheckInTask),
'score ledger should include daily check-in task source',
);
assert.ok(
scoreEvents.items?.some(item => item.eventType === 'check_in' && item.sourceId === ids.streakCheckInTask),
'score ledger should include streak check-in task source',
);
const dailyCheckInClaims = await request('/api/tenant-admin/point-activity-claims', {
userId: TENANT_ADMIN_USER_ID,
query: { taskId: ids.dailyCheckInTask, limit: 20 },
});
assert.equal(
dailyCheckInClaims.items?.filter(item => item.userId === USER_ID).length,
1,
'tenant admin should see one daily check-in claim for the student',
);
const autoProfileBadges = await request('/api/profile/badges', {
query: { includeLocked: 'true', category: 'activity', limit: 20 },