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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:26:37 +08:00

83 lines
2.8 KiB
TypeScript

import { z } from "zod"
export const achievementRaritySchema = z.enum(["bronze", "silver", "gold", "platinum"])
export const achievementSchema = z.object({
id: z.number().int(),
name: z.string(),
description: z.string(),
icon: z.string(),
rarity: achievementRaritySchema,
hidden: z.boolean(),
metric: z.string().nullable(),
operator: z.enum(["gte", "lte"]).nullable(),
threshold: z.number().int().nullable(),
unlocked: z.boolean(),
unlockTime: z.string().nullable(),
backfilled: z.boolean(),
/** 连续几周那一串给的是「现在连着几周」,不是历史最长 —— 要拿下一档得从现在数起 */
progress: z.number().nullable(),
/** 全站几人有 */
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({
id: z.number().int(),
name: z.string(),
description: z.string(),
icon: z.string(),
rarity: achievementRaritySchema,
})
/**
* WebSocket 推来的解锁通知。比 `/achievements/pending` 多一个 `kind` ——
* 题单奖章和成就来自两张表、id 会重叠,前端靠它区分(见 events.ts 的 publishAchievementNotification)。
*/
export const achievementNotificationSchema = pendingAchievementSchema.extend({
kind: z.enum(["achievement", "badge"]),
})
export const markAchievementsReadSchema = z.object({
ids: z.array(z.number().int()),
})
export type Achievement = z.infer<typeof achievementSchema>
export type AchievementList = z.infer<typeof achievementListSchema>
export type PendingAchievement = z.infer<typeof pendingAchievementSchema>
export type AchievementClassBoard = z.infer<typeof achievementClassBoardSchema>
export type AchievementRarity = z.infer<typeof achievementRaritySchema>
export type AchievementNotification = z.infer<typeof achievementNotificationSchema>
export type MarkAchievementsRead = z.infer<typeof markAchievementsReadSchema>