feat(成就): 下架三个拿不到的成就、理顺稀有度和图标;接口多给本班几人有和「本班」卡片

- 0032:下架六边形战士(判题机只剩 3 种语言)、坚持三天/一周(按天算基本拿不到);
  不撞南墙改白银、屡败屡战改黄金;百发百中换 💯、屡败屡战换 💪,不再和别的撞图标;
  代码行数只算做对的、空行不算,并按新口径重算所有人;已经拿到的一个不收回
- 奖杯收藏家只数上架的成就(判题结算、补发重算、0032 存量一起改)
- GET /achievements:每个成就带全站几人有、本班几人有;看自己时带「本班」(最近拿到、成就最多前五、自己第几);
  隐藏成就同一串拿到一档就整串亮出来;连续几周给「现在连着几周」而不是历史最长
- 删掉只有旧成就页在用的 /achievements/summary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 00:26:37 +08:00
1 parent 689d40b116
commit 85108df9d8
9 files changed
+4220 -213

No files matched your search

+2 -2
View File
@@ -92,12 +92,12 @@ export const ACHIEVEMENT_METRICS: AchievementMetric[] = [
{
key: "max_code_lines",
name: "最长代码行数",
helpText: "提交过的最长代码有多少行",
helpText: "做对的提交里最长的代码有多少行(空行不算,不含比赛)",
},
{
key: "achievement_unlocked_count",
name: "已解锁成就数",
helpText: "已解锁的成就数量(不含白金档)",
helpText: "已解锁的成就数量(不含白金档和已下架的)",
meta: true,
},
]
+14 -7
View File
@@ -192,10 +192,12 @@ export async function updateAchievementsForSubmission(submissionId: string) {
value.early_bird_submissions = numberMetric(value, "early_bird_submissions") + 1
if (row.submission.result === JudgeStatus.COMPILE_ERROR)
value.compile_error_count = numberMetric(value, "compile_error_count") + 1
value.max_code_lines = Math.max(
numberMetric(value, "max_code_lines"),
row.submission.code.split(/\r?\n/).length,
)
// 只算做对的、空行不算:原来交什么都算,贴一大段就拿到「长篇巨制」(0032 按这个口径重算过存量)
if (accepted)
value.max_code_lines = Math.max(
numberMetric(value, "max_code_lines"),
row.submission.code.split("\n").filter((line) => line.trim() !== "").length,
)
await tx
.update(schema.userStat)
.set({ metrics: value, updateTime: new Date().toISOString() })
@@ -213,6 +215,7 @@ export async function updateAchievementsForSubmission(submissionId: string) {
and(
eq(schema.userAchievement.userId, row.submission.userId),
ne(schema.achievement.rarity, "platinum"),
eq(schema.achievement.visible, true),
),
)
metrics.achievement_unlocked_count = meta?.value ?? 0
@@ -269,7 +272,11 @@ export async function updateAchievementsForProblemSet(userId: number) {
.from(schema.userAchievement)
.innerJoin(schema.achievement, eq(schema.userAchievement.achievementId, schema.achievement.id))
.where(
and(eq(schema.userAchievement.userId, userId), ne(schema.achievement.rarity, "platinum")),
and(
eq(schema.userAchievement.userId, userId),
ne(schema.achievement.rarity, "platinum"),
eq(schema.achievement.visible, true),
),
)
metrics.achievement_unlocked_count = meta?.value ?? 0
await db
@@ -406,7 +413,7 @@ export function metaAchievements() {
/**
* 按 `user_achievement` 重算 `achievement_unlocked_count`,返回实际改动了的用户 id。
*
* 口径和判题结算一致:已解锁的**非白金**成就数。只 `jsonb_set` 这一个键、只写值变了的行,
* 口径和判题结算一致:已解锁的、还上架的**非白金**成就数(下架的不算,见 0032)。只 `jsonb_set` 这一个键、只写值变了的行,
* 不整体覆盖 `metrics` —— 整体写回会和并发判题写的其它指标互相踩。
* 不传 `userIds` 就是全体有 `user_stat` 的用户(`recount` 存量订正用)。
*/
@@ -423,7 +430,7 @@ export async function refreshUnlockedCount(userIds?: number[]) {
select ua.user_id, count(*)::int as value
from ${schema.userAchievement} ua
join ${schema.achievement} a on a.id = ua.achievement_id
where a.rarity <> 'platinum'
where a.rarity <> 'platinum' and a.visible
group by ua.user_id
) c on c.user_id = s.user_id
where true ${scope}
+14 -7
View File
@@ -99,6 +99,19 @@ function numberOf(metrics: Record<string, unknown>, key: string) {
return typeof value === "number" ? value : 0
}
/**
* 现在连着几周有新做对。`max_ac_week_streak` 是历史最长,要拿「锲而不舍」得从现在的连续数起;
* 只在最近一次新通过落在本周或上周时还作数,再早就已经断了。
*/
export function currentWeekStreak(metrics: Record<string, unknown>) {
const monday = calendarDay(weekStart())
const lastWeek = typeof metrics._last_ac_week === "string" ? metrics._last_ac_week : ""
const previousMonday = dayText(dayNumber(monday) - 7)
return lastWeek === monday || lastWeek === previousMonday
? numberOf(metrics, "_current_ac_week_streak")
: 0
}
export async function buildProblemProgress(userId: number): Promise<ProblemProgress> {
const [states, statRows, achievements, unlockedRows] = await Promise.all([
problemStates(userId),
@@ -159,13 +172,7 @@ export async function buildProblemProgress(userId: number): Promise<ProblemProgr
for (const [day, n] of Object.entries(asRecord(metrics._ac_per_day))) {
if (day >= monday && typeof n === "number") weekSolved += n
}
// 当前连续周数只在最近一次新通过落在本周或上周时还作数,再早就已经断了
const lastWeek = typeof metrics._last_ac_week === "string" ? metrics._last_ac_week : ""
const previousMonday = dayText(dayNumber(monday) - 7)
const weekStreak =
lastWeek === monday || lastWeek === previousMonday
? numberOf(metrics, "_current_ac_week_streak")
: 0
const weekStreak = currentWeekStreak(metrics)
const unlocked = new Set(unlockedRows.map((row) => row.id))
let next: ProblemProgress["next"] = null