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

+30 -19
View File
@@ -15,13 +15,41 @@ export const achievementSchema = z.object({
unlocked: z.boolean(),
unlockTime: z.string().nullable(),
backfilled: z.boolean(),
/** 连续几周那一串给的是「现在连着几周」,不是历史最长 —— 要拿下一档得从现在数起 */
progress: z.number().nullable(),
unlockRate: z.number(),
/** 全站几人有 */
holders: z.number().int(),
/** 本班(被看的人的班)几人有;没有班级是 null */
classHolders: z.number().int().nullable(),
})
/** 「本班」那张卡:只在看自己、而且有班级时给 */
export const achievementClassBoardSchema = z.object({
className: z.string(),
/** 本班最近拿到的(补发的不算),新的在前 */
recent: z.array(
z.object({
username: z.string(),
achievementId: z.number().int(),
unlockTime: z.string(),
}),
),
/** 本班成就最多的前几名;一样多的名次相同,先拿到的排前面 */
most: z.array(
z.object({
username: z.string(),
count: z.number().int(),
rank: z.number().int(),
}),
),
/** 自己不在 most 里时的名次;不计入排名的人是 null */
me: z.object({ rank: z.number().int(), count: z.number().int() }).nullable(),
})
export const achievementListSchema = z.object({
username: z.string(),
achievements: z.array(achievementSchema),
classBoard: achievementClassBoardSchema.nullable(),
})
export const pendingAchievementSchema = z.object({
@@ -32,13 +60,6 @@ export const pendingAchievementSchema = z.object({
rarity: achievementRaritySchema,
})
export const achievementRarityStatSchema = z.object({
rarity: achievementRaritySchema,
label: z.string(),
total: z.number().int(),
unlocked: z.number().int(),
})
/**
* WebSocket 推来的解锁通知。比 `/achievements/pending` 多一个 `kind` ——
* 题单奖章和成就来自两张表、id 会重叠,前端靠它区分(见 events.ts 的 publishAchievementNotification)。
@@ -47,15 +68,6 @@ export const achievementNotificationSchema = pendingAchievementSchema.extend({
kind: z.enum(["achievement", "badge"]),
})
export const achievementSummarySchema = z.object({
username: z.string(),
total: z.number().int(),
unlocked: z.number().int(),
percent: z.number(),
rarity: z.array(achievementRarityStatSchema),
recent: z.array(pendingAchievementSchema),
})
export const markAchievementsReadSchema = z.object({
ids: z.array(z.number().int()),
})
@@ -63,9 +75,8 @@ export const markAchievementsReadSchema = z.object({
export type Achievement = z.infer<typeof achievementSchema>
export type AchievementList = z.infer<typeof achievementListSchema>
export type PendingAchievement = z.infer<typeof pendingAchievementSchema>
export type AchievementSummary = z.infer<typeof achievementSummarySchema>
export type AchievementClassBoard = z.infer<typeof achievementClassBoardSchema>
export type AchievementRarity = z.infer<typeof achievementRaritySchema>
export type AchievementRarityStat = z.infer<typeof achievementRarityStatSchema>
export type AchievementNotification = z.infer<typeof achievementNotificationSchema>
export type MarkAchievementsRead = z.infer<typeof markAchievementsReadSchema>