forked from wangziqi/gongxue-base
feat: add automatic badge awards
This commit is contained in:
@@ -52,6 +52,9 @@ const ids = {
|
||||
examDate: '00000000-0000-0000-0000-000000000861',
|
||||
tenantExamDate: '00000000-0000-0000-0000-000000000862',
|
||||
tenantBadge: '00000000-0000-0000-0000-000000000871',
|
||||
tenantCheckInBadge: '00000000-0000-0000-0000-000000000873',
|
||||
tenantScoreBadge: '00000000-0000-0000-0000-000000000874',
|
||||
tenantFeedbackBadge: '00000000-0000-0000-0000-000000000875',
|
||||
questionBank: '00000000-0000-0000-0000-000000000400',
|
||||
publicQuestionBankGrant: '00000000-0000-0000-0000-000000000906',
|
||||
};
|
||||
@@ -1685,20 +1688,87 @@ async function testProfile() {
|
||||
assert.equal(typeof profileExamDate?.daysLeft, 'number', 'profile countdown should include daysLeft');
|
||||
assert.equal(countdowns.target?.regionId, ids.region, 'profile countdown should use student target region');
|
||||
|
||||
const checkInBadge = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.tenantCheckInBadge,
|
||||
legacyId: 'integration-badge-check-in-auto',
|
||||
name: '集成测试签到勋章',
|
||||
description: '连续签到达到规则后自动发放',
|
||||
category: 'activity',
|
||||
iconUrl: 'https://example.test/badges/check-in-auto.png',
|
||||
level: 1,
|
||||
unlockType: 'check_in',
|
||||
conditionField: 'checkInStreak',
|
||||
conditionOperator: 'gte',
|
||||
conditionValue: 1,
|
||||
metadata: { source: 'integration-test', trigger: 'check_in' },
|
||||
order: 1,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
assert.equal(checkInBadge.item?.id, ids.tenantCheckInBadge, 'tenant admin should create automatic check-in badge');
|
||||
|
||||
const scoreBadge = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.tenantScoreBadge,
|
||||
legacyId: 'integration-badge-score-auto',
|
||||
name: '集成测试积分勋章',
|
||||
description: '积分达到规则后自动发放',
|
||||
category: 'activity',
|
||||
iconUrl: 'https://example.test/badges/score-auto.png',
|
||||
level: 1,
|
||||
unlockType: 'score',
|
||||
conditionField: 'score',
|
||||
conditionOperator: 'gte',
|
||||
conditionValue: 10,
|
||||
metadata: { source: 'integration-test', trigger: 'score' },
|
||||
order: 2,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
assert.equal(scoreBadge.item?.id, ids.tenantScoreBadge, 'tenant admin should create automatic score badge');
|
||||
|
||||
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.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.ok(
|
||||
checkIn.item?.autoBadges?.some(item => item.badgeId === ids.tenantCheckInBadge),
|
||||
'check-in should auto grant check-in badge',
|
||||
);
|
||||
assert.ok(
|
||||
checkIn.item?.autoBadges?.some(item => item.badgeId === ids.tenantScoreBadge),
|
||||
'check-in should auto grant score threshold badge',
|
||||
);
|
||||
|
||||
const duplicateCheckIn = await request('/api/profile/check-in', { method: 'POST' });
|
||||
assert.equal(duplicateCheckIn.item?.checkedIn, false, 'duplicate daily check-in should be idempotent');
|
||||
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');
|
||||
|
||||
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');
|
||||
|
||||
const autoProfileBadges = await request('/api/profile/badges', {
|
||||
query: { includeLocked: 'true', category: 'activity', limit: 20 },
|
||||
});
|
||||
assert.equal(
|
||||
autoProfileBadges.items?.find(item => item.badgeId === ids.tenantCheckInBadge)?.unlocked,
|
||||
true,
|
||||
'profile badges should expose auto check-in badge as unlocked',
|
||||
);
|
||||
assert.equal(
|
||||
autoProfileBadges.items?.find(item => item.badgeId === ids.tenantScoreBadge)?.unlocked,
|
||||
true,
|
||||
'profile badges should expose auto score badge as unlocked',
|
||||
);
|
||||
|
||||
const feedback = await request('/api/profile/feedbacks', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -1815,6 +1885,7 @@ async function testAiSchoolRecommendationAfterSvip() {
|
||||
method: 'POST',
|
||||
body: {
|
||||
regionId: ids.region,
|
||||
targetSchoolId: ids.scorelineSchool,
|
||||
estimatedScore: 210,
|
||||
riskPreference: 'balanced',
|
||||
constraints: '优先考虑计算机相关专业',
|
||||
@@ -6566,6 +6637,28 @@ async function testTenantAdminOps() {
|
||||
});
|
||||
assert.equal(studentFeedbackDenied.code, 'TENANT_ADMIN_REQUIRED', 'student should not access tenant feedback admin');
|
||||
|
||||
const feedbackBadge = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.tenantFeedbackBadge,
|
||||
legacyId: 'integration-badge-feedback-auto',
|
||||
name: '集成测试纠错勋章',
|
||||
description: '反馈被解决达到规则后自动发放',
|
||||
category: 'feedback',
|
||||
iconUrl: 'https://example.test/badges/feedback-auto.png',
|
||||
level: 1,
|
||||
unlockType: 'feedback_resolved',
|
||||
conditionField: 'feedbackResolvedCount',
|
||||
conditionOperator: 'gte',
|
||||
conditionValue: 1,
|
||||
metadata: { source: 'integration-test', trigger: 'feedback_resolved' },
|
||||
order: 1,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
assert.equal(feedbackBadge.item?.id, ids.tenantFeedbackBadge, 'tenant admin should create automatic feedback badge');
|
||||
|
||||
const resolvedFeedback = await request('/api/tenant-admin/feedbacks/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
@@ -6581,6 +6674,10 @@ async function testTenantAdminOps() {
|
||||
});
|
||||
assert.equal(resolvedFeedback.item?.status, 'resolved', 'tenant admin should resolve feedback');
|
||||
assert.equal(resolvedFeedback.item?.reward?.eventType, 'feedback_reward', 'feedback reward should write score ledger');
|
||||
assert.ok(
|
||||
resolvedFeedback.item?.autoBadges?.some(item => item.badgeId === ids.tenantFeedbackBadge),
|
||||
'resolved feedback should auto grant feedback badge',
|
||||
);
|
||||
|
||||
const rewardEventsAfterResolve = await request('/api/profile/score-events', { query: { limit: 50 } });
|
||||
const rewardCountAfterResolve = rewardEventsAfterResolve.items?.filter(
|
||||
@@ -6600,6 +6697,11 @@ async function testTenantAdminOps() {
|
||||
},
|
||||
});
|
||||
assert.equal(repeatedReward.item?.reward, null, 'repeated feedback reward should be idempotent');
|
||||
assert.equal(
|
||||
repeatedReward.item?.autoBadges?.filter(item => item.badgeId === ids.tenantFeedbackBadge).length || 0,
|
||||
0,
|
||||
'repeated feedback resolution should not duplicate auto feedback badge',
|
||||
);
|
||||
|
||||
const rewardEventsAfterRepeat = await request('/api/profile/score-events', { query: { limit: 50 } });
|
||||
const rewardCountAfterRepeat = rewardEventsAfterRepeat.items?.filter(
|
||||
@@ -6607,6 +6709,15 @@ async function testTenantAdminOps() {
|
||||
).length || 0;
|
||||
assert.equal(rewardCountAfterRepeat, 1, 'feedback reward should not duplicate on repeated status updates');
|
||||
|
||||
const feedbackProfileBadges = await request('/api/profile/badges', {
|
||||
query: { includeLocked: 'true', category: 'feedback', limit: 20 },
|
||||
});
|
||||
assert.equal(
|
||||
feedbackProfileBadges.items?.find(item => item.badgeId === ids.tenantFeedbackBadge)?.unlocked,
|
||||
true,
|
||||
'profile badges should expose auto feedback badge as unlocked',
|
||||
);
|
||||
|
||||
const feedbackEvents = await request('/api/tenant-admin/feedbacks/events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { reportId: pendingFeedback.id },
|
||||
|
||||
@@ -70,6 +70,10 @@ const ids = {
|
||||
tenantClassOther: '00000000-0000-0000-0000-000000000852',
|
||||
examDate: '00000000-0000-0000-0000-000000000861',
|
||||
examDateSchool: '00000000-0000-0000-0000-000000000862',
|
||||
tenantBadge: '00000000-0000-0000-0000-000000000871',
|
||||
tenantCheckInBadge: '00000000-0000-0000-0000-000000000873',
|
||||
tenantScoreBadge: '00000000-0000-0000-0000-000000000874',
|
||||
tenantFeedbackBadge: '00000000-0000-0000-0000-000000000875',
|
||||
partnerTenant: '00000000-0000-0000-0000-000000000901',
|
||||
partnerSubscription: '00000000-0000-0000-0000-000000000902',
|
||||
partnerInvoice: '00000000-0000-0000-0000-000000000903',
|
||||
@@ -230,6 +234,66 @@ async function main() {
|
||||
[tenantId, ids.user, ids.tenantAdminUser, ids.secondStudentUser, ids.leaderboardScoreEventSecond],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.audit_logs
|
||||
where tenant_id = $1
|
||||
and target_type = 'ai_recommendation_report'
|
||||
and target_id in (
|
||||
select id::text
|
||||
from public.ai_recommendation_reports
|
||||
where tenant_id = $1
|
||||
and user_id in ($2::uuid, $3::uuid)
|
||||
)
|
||||
`,
|
||||
[tenantId, ids.user, ids.secondStudentUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.ai_recommendation_reports
|
||||
where tenant_id = $1
|
||||
and user_id in ($2::uuid, $3::uuid)
|
||||
`,
|
||||
[tenantId, ids.user, ids.secondStudentUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.user_badges
|
||||
where tenant_id = $1
|
||||
and (
|
||||
user_id in ($2::uuid, $3::uuid)
|
||||
or badge_id = any($4::uuid[])
|
||||
or coalesce(legacy_id, '') = any($5::text[])
|
||||
)
|
||||
`,
|
||||
[
|
||||
tenantId,
|
||||
ids.user,
|
||||
ids.secondStudentUser,
|
||||
[ids.tenantBadge, ids.tenantCheckInBadge, ids.tenantScoreBadge, ids.tenantFeedbackBadge],
|
||||
[
|
||||
`badge:${ids.tenantBadge}:user:${ids.user}`,
|
||||
`auto:${ids.tenantCheckInBadge}:user:${ids.user}`,
|
||||
`auto:${ids.tenantScoreBadge}:user:${ids.user}`,
|
||||
`auto:${ids.tenantFeedbackBadge}:user:${ids.user}`,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.badges
|
||||
where tenant_id = $1
|
||||
and (
|
||||
id = any($2::uuid[])
|
||||
or coalesce(legacy_id, '') like 'integration-badge-%'
|
||||
)
|
||||
`,
|
||||
[tenantId, [ids.tenantBadge, ids.tenantCheckInBadge, ids.tenantScoreBadge, ids.tenantFeedbackBadge]],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.practice_session_report_sections
|
||||
|
||||
Reference in New Issue
Block a user