forked from wangziqi/gongxue-base
feat: configure daily check-in rewards
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -78,6 +78,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',
|
||||
@@ -280,7 +282,7 @@ async function main() {
|
||||
tenantId,
|
||||
ids.user,
|
||||
ids.secondStudentUser,
|
||||
[ids.pointActivityTask, ids.pointSystemTask],
|
||||
[ids.pointActivityTask, ids.pointSystemTask, ids.dailyCheckInTask, ids.streakCheckInTask],
|
||||
],
|
||||
);
|
||||
|
||||
@@ -319,7 +321,7 @@ async function main() {
|
||||
or code::text like 'integration-%'
|
||||
)
|
||||
`,
|
||||
[tenantId, [ids.pointActivityTask, ids.pointSystemTask]],
|
||||
[tenantId, [ids.pointActivityTask, ids.pointSystemTask, ids.dailyCheckInTask, ids.streakCheckInTask]],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
@@ -1000,7 +1002,7 @@ async function main() {
|
||||
set region_id = $3,
|
||||
avatar_preset = 'male',
|
||||
last_check_in_date = null,
|
||||
stats = stats - 'checkInStreak' - 'lastCheckInPoints',
|
||||
stats = stats - 'checkInStreak' - 'lastCheckInPoints' - 'lastCheckInRewardMode' - 'checkInRewardBreakdown',
|
||||
updated_at = now()
|
||||
where tenant_id = $1
|
||||
and user_id in ($2::uuid, $4::uuid)
|
||||
|
||||
Reference in New Issue
Block a user