feat(个人主页): 后端一个接口给齐主页要的东西:做对的题带题名和日期、四个时间、本班第几、成就,老师和本人多拿知识点和没做对的题

- GET /profiles/:username/overview:同学看拿不到知识点和没做对的题,多一份「你和他」
- DELETE /me/avatar:不用图片,退回名字的字
- 知识点地图抽成 knowledgeMapOf,老师看学生也能用
- 删掉没人用了的 /users/:id/metrics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 00:03:37 +08:00
1 parent afbc240a19
commit cbf5b9d922
3 files changed
+272 -127

No files matched your search

+70 -9
View File
@@ -1,6 +1,7 @@
import { z } from "zod"
import { sampleUserSchema } from "./common"
import { achievementRaritySchema } from "./achievement"
import { userProfileSchema } from "./auth"
/** 个性签名最多几个字。以前是 256,排名上要露出来后改成 30(九成签名本来就不超过 26 字) */
@@ -19,14 +20,6 @@ export const updateProfileRequestSchema = z.object({
mood: z.string().trim().max(MOOD_MAX, `个性签名最多 ${MOOD_MAX} 个字`).nullable().optional(),
})
export const metricsSchema = z.object({
now: z.string(),
latest: z.string(),
first: z.string(),
/** 有提交的日历天数(东八区),不是首末提交之间跨了多少天 */
activeDays: z.number().int(),
})
export const rankProfileSchema = z.object({
id: z.number().int(),
user: sampleUserSchema,
@@ -84,7 +77,6 @@ export type ProblemRank = z.infer<typeof problemRankSchema>
export type RankProfile = z.infer<typeof rankProfileSchema>
export type WeeklyRankItem = z.infer<typeof weeklyRankItemSchema>
export type WeeklyRank = z.infer<typeof weeklyRankSchema>
export type Metrics = z.infer<typeof metricsSchema>
export type PublicProfile = z.infer<typeof publicProfileSchema>
@@ -120,3 +112,72 @@ export const knowledgeMapSchema = z.object({
export type KnowledgeLevel = z.infer<typeof knowledgeLevelSchema>
export type KnowledgeMap = z.infer<typeof knowledgeMapSchema>
/**
* 个人主页(`/profiles/:username/overview`,设计稿「我的主页与设置」定稿)。
*
* 看自己、同学看、老师看是同一份接口,差别在后端按看的人裁:`unsolved` 和 `knowledge`
* 只给本人和老师(知识点不开放给同学 —— 开放了就成了另一张排行榜),同学拿到的是 null。
*/
export const profileSolvedSchema = z.object({
displayId: z.string(),
title: z.string(),
/** 第一次做对的时刻,按它分天 */
solvedAt: z.string(),
/** 知识点标签(category = knowledge),「按知识点」看时按它分组 */
tags: z.array(z.string()),
})
export const profileUnsolvedSchema = z.object({
displayId: z.string(),
title: z.string(),
attempts: z.number().int(),
})
export const profileAchievementSchema = z.object({
id: z.number().int(),
name: z.string(),
description: z.string(),
icon: z.string(),
rarity: achievementRaritySchema,
unlockTime: z.string(),
})
export const profileOverviewSchema = z.object({
/** 四个时间(用户 2026-10-12 要求保留)。一次没交过时前两个为 null、activeDays 为 0 */
firstSubmissionAt: z.string().nullable(),
latestSubmissionAt: z.string().nullable(),
/** 有提交的日历天数(东八区),比赛提交也算 */
activeDays: z.number().int(),
now: z.string(),
/** 这学期在本班第几(和排名页同口径)。没班级、一道没做对、不计入排名、不是学生时为 null */
classRank: z.object({ rank: z.number().int(), total: z.number().int() }).nullable(),
/** 被老师设成不计入排名。只告诉本人和老师,同学看到的恒为 false */
rankHidden: z.boolean(),
/** 做对的题,新的在前;比赛里的提交不算 */
solved: z.array(profileSolvedSchema),
/** 交过还没做对的题,最近交的在前;同学看时为 null */
unsolved: z.array(profileUnsolvedSchema).nullable(),
knowledge: knowledgeMapSchema.nullable(),
/**
* 同学看他时的「你和他」:你做对几道、其中几道他也做对了、你在本班第几(同班才有)。
* 本人、老师看时为 null
*/
versus: z
.object({
solved: z.number().int(),
both: z.number().int(),
classRank: z.object({ rank: z.number().int(), total: z.number().int() }).nullable(),
})
.nullable(),
achievements: z.object({
total: z.number().int(),
/** 解锁了的,新的在前 */
unlocked: z.array(profileAchievementSchema),
}),
})
export type ProfileSolved = z.infer<typeof profileSolvedSchema>
export type ProfileUnsolved = z.infer<typeof profileUnsolvedSchema>
export type ProfileAchievement = z.infer<typeof profileAchievementSchema>
export type ProfileOverview = z.infer<typeof profileOverviewSchema>